dm-core 0.10.2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. data/.gitignore +10 -1
  2. data/Gemfile +143 -0
  3. data/Rakefile +9 -5
  4. data/VERSION +1 -1
  5. data/dm-core.gemspec +160 -57
  6. data/lib/dm-core.rb +131 -56
  7. data/lib/dm-core/adapters.rb +98 -14
  8. data/lib/dm-core/adapters/abstract_adapter.rb +24 -4
  9. data/lib/dm-core/adapters/in_memory_adapter.rb +7 -2
  10. data/lib/dm-core/associations/many_to_many.rb +19 -30
  11. data/lib/dm-core/associations/many_to_one.rb +58 -42
  12. data/lib/dm-core/associations/one_to_many.rb +33 -23
  13. data/lib/dm-core/associations/one_to_one.rb +27 -11
  14. data/lib/dm-core/associations/relationship.rb +4 -4
  15. data/lib/dm-core/collection.rb +23 -16
  16. data/lib/dm-core/core_ext/array.rb +36 -0
  17. data/lib/dm-core/core_ext/hash.rb +30 -0
  18. data/lib/dm-core/core_ext/module.rb +46 -0
  19. data/lib/dm-core/core_ext/object.rb +31 -0
  20. data/lib/dm-core/core_ext/pathname.rb +20 -0
  21. data/lib/dm-core/core_ext/string.rb +22 -0
  22. data/lib/dm-core/core_ext/try_dup.rb +44 -0
  23. data/lib/dm-core/model.rb +88 -27
  24. data/lib/dm-core/model/hook.rb +75 -18
  25. data/lib/dm-core/model/property.rb +50 -9
  26. data/lib/dm-core/model/relationship.rb +31 -31
  27. data/lib/dm-core/model/scope.rb +3 -3
  28. data/lib/dm-core/property.rb +196 -516
  29. data/lib/dm-core/property/binary.rb +7 -0
  30. data/lib/dm-core/property/boolean.rb +35 -0
  31. data/lib/dm-core/property/class.rb +24 -0
  32. data/lib/dm-core/property/date.rb +47 -0
  33. data/lib/dm-core/property/date_time.rb +48 -0
  34. data/lib/dm-core/property/decimal.rb +43 -0
  35. data/lib/dm-core/property/discriminator.rb +48 -0
  36. data/lib/dm-core/property/float.rb +24 -0
  37. data/lib/dm-core/property/integer.rb +32 -0
  38. data/lib/dm-core/property/numeric.rb +43 -0
  39. data/lib/dm-core/property/object.rb +32 -0
  40. data/lib/dm-core/property/serial.rb +8 -0
  41. data/lib/dm-core/property/string.rb +49 -0
  42. data/lib/dm-core/property/text.rb +12 -0
  43. data/lib/dm-core/property/time.rb +48 -0
  44. data/lib/dm-core/property/typecast/numeric.rb +32 -0
  45. data/lib/dm-core/property/typecast/time.rb +28 -0
  46. data/lib/dm-core/property_set.rb +10 -4
  47. data/lib/dm-core/query.rb +14 -37
  48. data/lib/dm-core/query/conditions/comparison.rb +8 -6
  49. data/lib/dm-core/query/conditions/operation.rb +33 -2
  50. data/lib/dm-core/query/operator.rb +2 -5
  51. data/lib/dm-core/query/path.rb +4 -6
  52. data/lib/dm-core/repository.rb +21 -6
  53. data/lib/dm-core/resource.rb +316 -133
  54. data/lib/dm-core/resource/state.rb +79 -0
  55. data/lib/dm-core/resource/state/clean.rb +40 -0
  56. data/lib/dm-core/resource/state/deleted.rb +30 -0
  57. data/lib/dm-core/resource/state/dirty.rb +86 -0
  58. data/lib/dm-core/resource/state/immutable.rb +34 -0
  59. data/lib/dm-core/resource/state/persisted.rb +29 -0
  60. data/lib/dm-core/resource/state/transient.rb +70 -0
  61. data/lib/dm-core/spec/lib/adapter_helpers.rb +52 -0
  62. data/lib/dm-core/spec/lib/collection_helpers.rb +20 -0
  63. data/{spec → lib/dm-core/spec}/lib/counter_adapter.rb +5 -1
  64. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  65. data/lib/dm-core/spec/lib/spec_helper.rb +68 -0
  66. data/lib/dm-core/spec/setup.rb +165 -0
  67. data/lib/dm-core/spec/{adapter_shared_spec.rb → shared/adapter_spec.rb} +21 -7
  68. data/{spec/public/shared/resource_shared_spec.rb → lib/dm-core/spec/shared/resource_spec.rb} +120 -83
  69. data/{spec/public/shared/sel_shared_spec.rb → lib/dm-core/spec/shared/sel_spec.rb} +5 -6
  70. data/lib/dm-core/support/assertions.rb +8 -0
  71. data/lib/dm-core/support/equalizer.rb +1 -0
  72. data/lib/dm-core/support/hook.rb +420 -0
  73. data/lib/dm-core/support/lazy_array.rb +453 -0
  74. data/lib/dm-core/support/local_object_space.rb +12 -0
  75. data/lib/dm-core/support/logger.rb +193 -6
  76. data/lib/dm-core/support/naming_conventions.rb +8 -8
  77. data/lib/dm-core/support/subject.rb +33 -0
  78. data/lib/dm-core/type.rb +4 -0
  79. data/lib/dm-core/types/boolean.rb +2 -0
  80. data/lib/dm-core/types/decimal.rb +9 -0
  81. data/lib/dm-core/types/discriminator.rb +2 -0
  82. data/lib/dm-core/types/object.rb +3 -0
  83. data/lib/dm-core/types/serial.rb +2 -0
  84. data/lib/dm-core/types/text.rb +2 -0
  85. data/lib/dm-core/version.rb +1 -1
  86. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +67 -0
  87. data/spec/public/model/hook_spec.rb +209 -0
  88. data/spec/public/model/property_spec.rb +35 -0
  89. data/spec/public/model/relationship_spec.rb +33 -20
  90. data/spec/public/model_spec.rb +142 -10
  91. data/spec/public/property/binary_spec.rb +14 -0
  92. data/spec/public/property/boolean_spec.rb +14 -0
  93. data/spec/public/property/class_spec.rb +20 -0
  94. data/spec/public/property/date_spec.rb +14 -0
  95. data/spec/public/property/date_time_spec.rb +14 -0
  96. data/spec/public/property/decimal_spec.rb +14 -0
  97. data/spec/public/{types → property}/discriminator_spec.rb +2 -12
  98. data/spec/public/property/float_spec.rb +14 -0
  99. data/spec/public/property/integer_spec.rb +14 -0
  100. data/spec/public/property/object_spec.rb +9 -17
  101. data/spec/public/property/serial_spec.rb +14 -0
  102. data/spec/public/property/string_spec.rb +14 -0
  103. data/spec/public/property/text_spec.rb +52 -0
  104. data/spec/public/property/time_spec.rb +14 -0
  105. data/spec/public/property_spec.rb +28 -87
  106. data/spec/public/resource_spec.rb +101 -0
  107. data/spec/public/sel_spec.rb +5 -15
  108. data/spec/public/shared/collection_shared_spec.rb +16 -30
  109. data/spec/public/shared/finder_shared_spec.rb +2 -4
  110. data/spec/public/shared/property_shared_spec.rb +176 -0
  111. data/spec/semipublic/adapters/abstract_adapter_spec.rb +1 -1
  112. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +2 -2
  113. data/spec/semipublic/associations/many_to_many_spec.rb +89 -0
  114. data/spec/semipublic/associations/many_to_one_spec.rb +24 -1
  115. data/spec/semipublic/associations/one_to_many_spec.rb +51 -0
  116. data/spec/semipublic/associations/one_to_one_spec.rb +49 -0
  117. data/spec/semipublic/associations/relationship_spec.rb +3 -3
  118. data/spec/semipublic/associations_spec.rb +1 -1
  119. data/spec/semipublic/property/binary_spec.rb +13 -0
  120. data/spec/semipublic/property/boolean_spec.rb +65 -0
  121. data/spec/semipublic/property/class_spec.rb +33 -0
  122. data/spec/semipublic/property/date_spec.rb +43 -0
  123. data/spec/semipublic/property/date_time_spec.rb +46 -0
  124. data/spec/semipublic/property/decimal_spec.rb +82 -0
  125. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  126. data/spec/semipublic/property/float_spec.rb +82 -0
  127. data/spec/semipublic/property/integer_spec.rb +82 -0
  128. data/spec/semipublic/property/serial_spec.rb +13 -0
  129. data/spec/semipublic/property/string_spec.rb +13 -0
  130. data/spec/semipublic/property/text_spec.rb +31 -0
  131. data/spec/semipublic/property/time_spec.rb +50 -0
  132. data/spec/semipublic/property_spec.rb +2 -532
  133. data/spec/semipublic/query/conditions/comparison_spec.rb +171 -169
  134. data/spec/semipublic/query/conditions/operation_spec.rb +53 -51
  135. data/spec/semipublic/query/path_spec.rb +17 -17
  136. data/spec/semipublic/query_spec.rb +47 -78
  137. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  138. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  139. data/spec/semipublic/resource/state/dirty_spec.rb +133 -0
  140. data/spec/semipublic/resource/state/immutable_spec.rb +99 -0
  141. data/spec/semipublic/resource/state/transient_spec.rb +128 -0
  142. data/spec/semipublic/resource/state_spec.rb +226 -0
  143. data/spec/semipublic/shared/property_shared_spec.rb +143 -0
  144. data/spec/semipublic/shared/resource_shared_spec.rb +16 -15
  145. data/spec/semipublic/shared/resource_state_shared_spec.rb +78 -0
  146. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  147. data/spec/spec_helper.rb +21 -97
  148. data/spec/support/types/huge_integer.rb +17 -0
  149. data/spec/unit/array_spec.rb +48 -0
  150. data/spec/unit/hash_spec.rb +35 -0
  151. data/spec/unit/hook_spec.rb +1234 -0
  152. data/spec/unit/lazy_array_spec.rb +1959 -0
  153. data/spec/unit/module_spec.rb +70 -0
  154. data/spec/unit/object_spec.rb +37 -0
  155. data/spec/unit/try_dup_spec.rb +45 -0
  156. data/tasks/local_gemfile.rake +18 -0
  157. data/tasks/spec.rake +0 -3
  158. metadata +197 -71
  159. data/deps.rip +0 -2
  160. data/lib/dm-core/adapters/data_objects_adapter.rb +0 -712
  161. data/lib/dm-core/adapters/mysql_adapter.rb +0 -42
  162. data/lib/dm-core/adapters/oracle_adapter.rb +0 -229
  163. data/lib/dm-core/adapters/postgres_adapter.rb +0 -22
  164. data/lib/dm-core/adapters/sqlite3_adapter.rb +0 -17
  165. data/lib/dm-core/adapters/sqlserver_adapter.rb +0 -114
  166. data/lib/dm-core/adapters/yaml_adapter.rb +0 -111
  167. data/lib/dm-core/core_ext/enumerable.rb +0 -28
  168. data/lib/dm-core/migrations.rb +0 -1427
  169. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +0 -366
  170. data/lib/dm-core/transaction.rb +0 -508
  171. data/lib/dm-core/types/paranoid_boolean.rb +0 -42
  172. data/lib/dm-core/types/paranoid_datetime.rb +0 -41
  173. data/spec/lib/adapter_helpers.rb +0 -105
  174. data/spec/lib/collection_helpers.rb +0 -18
  175. data/spec/lib/pending_helpers.rb +0 -46
  176. data/spec/public/migrations_spec.rb +0 -503
  177. data/spec/public/transaction_spec.rb +0 -153
  178. data/spec/semipublic/adapters/mysql_adapter_spec.rb +0 -17
  179. data/spec/semipublic/adapters/oracle_adapter_spec.rb +0 -194
  180. data/spec/semipublic/adapters/postgres_adapter_spec.rb +0 -17
  181. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +0 -17
  182. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +0 -17
  183. data/spec/semipublic/adapters/yaml_adapter_spec.rb +0 -12
@@ -10,11 +10,11 @@ module DataMapper
10
10
  # TODO: replace this with BasicObject
11
11
  instance_methods.each do |method|
12
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)
13
+ %w[ send class dup object_id kind_of? instance_of? respond_to? respond_to_missing? equal? freeze frozen? should should_not instance_variables instance_variable_set instance_variable_get instance_variable_defined? remove_instance_variable extend hash inspect copy_object initialize_dup ].include?(method.to_s)
14
14
  undef_method method
15
15
  end
16
16
 
17
- include Extlib::Assertions
17
+ include DataMapper::Assertions
18
18
  extend Equalizer
19
19
 
20
20
  equalize :relationships, :property
@@ -62,16 +62,14 @@ module DataMapper
62
62
 
63
63
  # @api semipublic
64
64
  def initialize(relationships, property_name = nil)
65
- assert_kind_of 'relationships', relationships, Array
66
- assert_kind_of 'property_name', property_name, Symbol, NilClass
67
-
68
- @relationships = relationships.dup
65
+ @relationships = relationships.to_ary.dup
69
66
 
70
67
  last_relationship = @relationships.last
71
68
  @repository_name = last_relationship.relative_target_repository_name
72
69
  @model = last_relationship.target_model
73
70
 
74
71
  if property_name
72
+ property_name = property_name.to_sym
75
73
  @property = @model.properties(@repository_name)[property_name] ||
76
74
  raise(ArgumentError, "Unknown property '#{property_name}' in #{@model}")
77
75
  end
@@ -1,9 +1,9 @@
1
1
  module DataMapper
2
2
  class Repository
3
- include Extlib::Assertions
3
+ include DataMapper::Assertions
4
4
  extend Equalizer
5
5
 
6
- equalize :name, :adapter
6
+ equalize :name
7
7
 
8
8
  # Get the list of adapters registered for all Repositories,
9
9
  # keyed by repository name.
@@ -45,6 +45,9 @@ module DataMapper
45
45
  # @api semipublic
46
46
  attr_reader :name
47
47
 
48
+ # @api semipublic
49
+ alias to_sym name
50
+
48
51
  # Get the adapter for this repository
49
52
  #
50
53
  # Lazy loads adapter setup from registered adapters
@@ -114,6 +117,20 @@ module DataMapper
114
117
  end
115
118
  end
116
119
 
120
+ # Create a Query or subclass instance for this repository.
121
+ #
122
+ # @param [Model] model
123
+ # the Model to retrieve results from
124
+ # @param [Hash] options
125
+ # the conditions and scope
126
+ #
127
+ # @return [Query]
128
+ #
129
+ # @api semipublic
130
+ def new_query(model, options = {})
131
+ adapter.new_query(self, model, options)
132
+ end
133
+
117
134
  # Create one or more resource instances in this repository.
118
135
  #
119
136
  # TODO: create example
@@ -159,7 +176,7 @@ module DataMapper
159
176
  #
160
177
  # @api semipublic
161
178
  def update(attributes, collection)
162
- return 0 unless collection.query.valid?
179
+ return 0 unless collection.query.valid? && attributes.any?
163
180
  adapter.update(attributes, collection)
164
181
  end
165
182
 
@@ -202,9 +219,7 @@ module DataMapper
202
219
  #
203
220
  # @api semipublic
204
221
  def initialize(name)
205
- assert_kind_of 'name', name, Symbol
206
-
207
- @name = name
222
+ @name = name.to_sym
208
223
  @identity_maps = {}
209
224
  end
210
225
  end # class Repository
@@ -1,6 +1,9 @@
1
+ # TODO: DRY up raise_on_save_failure with attr_accessor_with_default
2
+ # once AS branch is merged in
3
+
1
4
  module DataMapper
2
5
  module Resource
3
- include Extlib::Assertions
6
+ include DataMapper::Assertions
4
7
  extend Chainable
5
8
  extend Deprecate
6
9
 
@@ -24,20 +27,51 @@ module DataMapper
24
27
  Model.descendants
25
28
  end
26
29
 
30
+ # Return if Resource#save should raise an exception on save failures (per-resource)
31
+ #
32
+ # This delegates to model.raise_on_save_failure by default.
33
+ #
34
+ # user.raise_on_save_failure # => false
35
+ #
36
+ # @return [Boolean]
37
+ # true if a failure in Resource#save should raise an exception
38
+ #
39
+ # @api public
40
+ def raise_on_save_failure
41
+ if defined?(@raise_on_save_failure)
42
+ @raise_on_save_failure
43
+ else
44
+ model.raise_on_save_failure
45
+ end
46
+ end
47
+
48
+ # Specify if Resource#save should raise an exception on save failures (per-resource)
49
+ #
50
+ # @param [Boolean]
51
+ # a boolean that if true will cause Resource#save to raise an exception
52
+ #
53
+ # @return [Boolean]
54
+ # true if a failure in Resource#save should raise an exception
55
+ #
56
+ # @api public
57
+ def raise_on_save_failure=(raise_on_save_failure)
58
+ @raise_on_save_failure = raise_on_save_failure
59
+ end
60
+
27
61
  # Deprecated API for updating attributes and saving Resource
28
62
  #
29
63
  # @see #update
30
64
  #
31
65
  # @deprecated
32
66
  def update_attributes(attributes = {}, *allowed)
33
- model = self.model
34
- caller = caller[0]
67
+ model = self.model
68
+ call_stack = caller[0]
35
69
 
36
- warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller})"
70
+ warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{call_stack})"
37
71
 
38
72
  if allowed.any?
39
73
  warn "specifying allowed in #{model}#update_attributes is deprecated, " \
40
- "use Hash#only to filter the attributes in the caller (#{caller})"
74
+ "use Hash#only to filter the attributes in the caller (#{call_stack})"
41
75
  attributes = attributes.only(*allowed)
42
76
  end
43
77
 
@@ -55,6 +89,38 @@ module DataMapper
55
89
  # @api public
56
90
  alias_method :model, :class
57
91
 
92
+ # Get the persisted state for the resource
93
+ #
94
+ # @return [Resource::State]
95
+ # the current persisted state for the resource
96
+ #
97
+ # @api private
98
+ def persisted_state
99
+ @_state ||= Resource::State::Transient.new(self)
100
+ end
101
+
102
+ # Set the persisted state for the resource
103
+ #
104
+ # @param [Resource::State]
105
+ # the new persisted state for the resource
106
+ #
107
+ # @return [undefined]
108
+ #
109
+ # @api private
110
+ def persisted_state=(state)
111
+ @_state = state
112
+ end
113
+
114
+ # Test if the persisted state is set
115
+ #
116
+ # @return [Boolean]
117
+ # true if the persisted state is set
118
+ #
119
+ # @api private
120
+ def persisted_state?
121
+ defined?(@_state) ? true : false
122
+ end
123
+
58
124
  # Repository this resource belongs to in the context of this collection
59
125
  # or of the resource's class.
60
126
  #
@@ -98,7 +164,7 @@ module DataMapper
98
164
  #
99
165
  # @api public
100
166
  def new?
101
- !saved?
167
+ persisted_state.kind_of?(State::Transient)
102
168
  end
103
169
 
104
170
  # Checks if this Resource instance is saved
@@ -108,7 +174,7 @@ module DataMapper
108
174
  #
109
175
  # @api public
110
176
  def saved?
111
- @_saved == true
177
+ persisted_state.kind_of?(State::Persisted)
112
178
  end
113
179
 
114
180
  # Checks if this Resource instance is destroyed
@@ -118,7 +184,7 @@ module DataMapper
118
184
  #
119
185
  # @api public
120
186
  def destroyed?
121
- @_destroyed == true
187
+ readonly? && !key.nil?
122
188
  end
123
189
 
124
190
  # Checks if the resource has no changes to save
@@ -128,7 +194,7 @@ module DataMapper
128
194
  #
129
195
  # @api public
130
196
  def clean?
131
- !dirty?
197
+ persisted_state.kind_of?(State::Clean) || persisted_state.kind_of?(State::Immutable)
132
198
  end
133
199
 
134
200
  # Checks if the resource has unsaved changes
@@ -150,7 +216,7 @@ module DataMapper
150
216
  #
151
217
  # @api public
152
218
  def readonly?
153
- @_readonly == true
219
+ persisted_state.kind_of?(State::Immutable)
154
220
  end
155
221
 
156
222
  # Returns the value of the attribute.
@@ -185,7 +251,7 @@ module DataMapper
185
251
  #
186
252
  # @api public
187
253
  def attribute_get(name)
188
- properties[name].get(self)
254
+ persisted_state.get(properties[name])
189
255
  end
190
256
 
191
257
  alias [] attribute_get
@@ -222,13 +288,11 @@ module DataMapper
222
288
  # @param [Object] value
223
289
  # value to store
224
290
  #
225
- # @return [Object]
226
- # the value stored at that given attribute, nil if none,
227
- # and default if necessary
291
+ # @return [undefined]
228
292
  #
229
293
  # @api public
230
294
  def attribute_set(name, value)
231
- properties[name].set(self, value)
295
+ self.persisted_state = persisted_state.set(properties[name], value)
232
296
  end
233
297
 
234
298
  alias []= attribute_set
@@ -283,7 +347,7 @@ module DataMapper
283
347
  raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
284
348
  end
285
349
  when Associations::Relationship, Property
286
- name.set(self, value)
350
+ self.persisted_state = persisted_state.set(name, value)
287
351
  end
288
352
  end
289
353
  end
@@ -307,6 +371,8 @@ module DataMapper
307
371
  clear_subjects
308
372
  end
309
373
 
374
+ self.persisted_state = persisted_state.rollback
375
+
310
376
  self
311
377
  end
312
378
 
@@ -319,7 +385,7 @@ module DataMapper
319
385
  # true if resource and storage state match
320
386
  #
321
387
  # @api public
322
- def update(attributes = {})
388
+ def update(attributes)
323
389
  assert_update_clean_only(:update)
324
390
  self.attributes = attributes
325
391
  save
@@ -334,7 +400,7 @@ module DataMapper
334
400
  # true if resource and storage state match
335
401
  #
336
402
  # @api public
337
- def update!(attributes = {})
403
+ def update!(attributes)
338
404
  assert_update_clean_only(:update!)
339
405
  self.attributes = attributes
340
406
  save!
@@ -348,7 +414,9 @@ module DataMapper
348
414
  # @api public
349
415
  def save
350
416
  assert_not_destroyed(:save)
351
- _save(true)
417
+ retval = _save
418
+ assert_save_successful(:save, retval)
419
+ retval
352
420
  end
353
421
 
354
422
  # Save the instance and loaded, dirty associations to the data-store, bypassing hooks
@@ -359,7 +427,9 @@ module DataMapper
359
427
  # @api public
360
428
  def save!
361
429
  assert_not_destroyed(:save!)
362
- _save(false)
430
+ retval = _save(false)
431
+ assert_save_successful(:save!, retval)
432
+ retval
363
433
  end
364
434
 
365
435
  # Destroy the instance, remove it from the repository
@@ -369,7 +439,13 @@ module DataMapper
369
439
  #
370
440
  # @api public
371
441
  def destroy
372
- destroy!
442
+ return true if destroyed?
443
+ catch :halt do
444
+ before_destroy_hook
445
+ retval = _destroy
446
+ after_destroy_hook
447
+ retval
448
+ end
373
449
  end
374
450
 
375
451
  # Destroy the instance, remove it from the repository, bypassing hooks
@@ -380,15 +456,7 @@ module DataMapper
380
456
  # @api public
381
457
  def destroy!
382
458
  return true if destroyed?
383
-
384
- if saved?
385
- repository.delete(collection_for_self)
386
- reset
387
- @_readonly = true
388
- @_destroyed = true
389
- else
390
- false
391
- end
459
+ _destroy(false)
392
460
  end
393
461
 
394
462
  # Compares another Resource for equality
@@ -423,9 +491,7 @@ module DataMapper
423
491
  # @api public
424
492
  def ==(other)
425
493
  return true if equal?(other)
426
- other.respond_to?(:repository) &&
427
- other.respond_to?(:key) &&
428
- other.respond_to?(:clean?) &&
494
+ return false unless other.kind_of?(Resource) && model.base_model.equal?(other.model.base_model)
429
495
  cmp?(other, :==)
430
496
  end
431
497
 
@@ -443,14 +509,13 @@ module DataMapper
443
509
  def <=>(other)
444
510
  model = self.model
445
511
  unless other.kind_of?(model.base_model)
446
- raise ArgumentError, "Cannot compare a #{other.model} instance with a #{model} instance"
512
+ raise ArgumentError, "Cannot compare a #{other.class} instance with a #{model} instance"
447
513
  end
448
- cmp = 0
449
514
  model.default_order(repository_name).each do |direction|
450
515
  cmp = direction.get(self) <=> direction.get(other)
451
- break if cmp != 0
516
+ return cmp if cmp.nonzero?
452
517
  end
453
- cmp
518
+ 0
454
519
  end
455
520
 
456
521
  # Returns hash value of the object.
@@ -497,7 +562,11 @@ module DataMapper
497
562
  #
498
563
  # @api semipublic
499
564
  def original_attributes
500
- @_original_attributes ||= {}
565
+ if persisted_state.respond_to?(:original_attributes)
566
+ persisted_state.original_attributes.dup.freeze
567
+ else
568
+ {}.freeze
569
+ end
501
570
  end
502
571
 
503
572
  # Checks if an attribute has been loaded from the repository
@@ -546,24 +615,13 @@ module DataMapper
546
615
  dirty_attributes = {}
547
616
 
548
617
  original_attributes.each_key do |property|
549
- dirty_attributes[property] = property.value(property.get!(self))
618
+ next unless property.respond_to?(:value)
619
+ dirty_attributes[property] = property.dump(property.get!(self))
550
620
  end
551
621
 
552
622
  dirty_attributes
553
623
  end
554
624
 
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)
558
- #
559
- # @api private
560
- def reset
561
- @_saved = false
562
- remove_from_identity_map
563
- original_attributes.clear
564
- self
565
- end
566
-
567
625
  # Returns the Collection the Resource is associated with
568
626
  #
569
627
  # @return [nil]
@@ -609,29 +667,81 @@ module DataMapper
609
667
  #
610
668
  # @api semipublic
611
669
  def query
612
- Query.new(repository, model, :fields => fields, :conditions => conditions)
670
+ repository.new_query(model, :fields => fields, :conditions => conditions)
613
671
  end
614
672
 
615
673
  protected
616
674
 
617
- # Method for hooking callbacks on resource creation
675
+ # Method for hooking callbacks before resource saving
618
676
  #
619
- # @return [Boolean]
620
- # true if the create was successful, false if not
677
+ # @return [undefined]
621
678
  #
622
679
  # @api private
623
- def create_hook
624
- _create
680
+ def before_save_hook
681
+ execute_hooks_for(:before, :save)
625
682
  end
626
683
 
627
- # Method for hooking callbacks on resource updates
684
+ # Method for hooking callbacks after resource saving
628
685
  #
629
- # @return [Boolean]
630
- # true if the update was successful, false if not
686
+ # @return [undefined]
687
+ #
688
+ # @api private
689
+ def after_save_hook
690
+ execute_hooks_for(:after, :save)
691
+ end
692
+
693
+ # Method for hooking callbacks before resource creation
694
+ #
695
+ # @return [undefined]
696
+ #
697
+ # @api private
698
+ def before_create_hook
699
+ execute_hooks_for(:before, :create)
700
+ end
701
+
702
+ # Method for hooking callbacks after resource creation
703
+ #
704
+ # @return [undefined]
705
+ #
706
+ # @api private
707
+ def after_create_hook
708
+ execute_hooks_for(:after, :create)
709
+ end
710
+
711
+ # Method for hooking callbacks before resource updating
712
+ #
713
+ # @return [undefined]
631
714
  #
632
715
  # @api private
633
- def update_hook
634
- _update
716
+ def before_update_hook
717
+ execute_hooks_for(:before, :update)
718
+ end
719
+
720
+ # Method for hooking callbacks after resource updating
721
+ #
722
+ # @return [undefined]
723
+ #
724
+ # @api private
725
+ def after_update_hook
726
+ execute_hooks_for(:after, :update)
727
+ end
728
+
729
+ # Method for hooking callbacks before resource destruction
730
+ #
731
+ # @return [undefined]
732
+ #
733
+ # @api private
734
+ def before_destroy_hook
735
+ execute_hooks_for(:before, :destroy)
736
+ end
737
+
738
+ # Method for hooking callbacks after resource destruction
739
+ #
740
+ # @return [undefined]
741
+ #
742
+ # @api private
743
+ def after_destroy_hook
744
+ execute_hooks_for(:after, :destroy)
635
745
  end
636
746
 
637
747
  private
@@ -649,6 +759,15 @@ module DataMapper
649
759
  self.attributes = attributes
650
760
  end
651
761
 
762
+ # @api private
763
+ def initialize_copy(original)
764
+ instance_variables.each do |ivar|
765
+ instance_variable_set(ivar, instance_variable_get(ivar).try_dup)
766
+ end
767
+
768
+ self.persisted_state = persisted_state.class.new(self)
769
+ end
770
+
652
771
  # Returns name of the repository this object
653
772
  # was loaded from
654
773
  #
@@ -721,7 +840,6 @@ module DataMapper
721
840
  def reset_key
722
841
  properties.key.zip(key) do |property, value|
723
842
  property.set!(self, value)
724
- original_attributes.delete(property)
725
843
  end
726
844
  end
727
845
 
@@ -736,7 +854,6 @@ module DataMapper
736
854
  (model_properties - model_properties.key | relationships.values).each do |subject|
737
855
  next unless subject.loaded?(self)
738
856
  remove_instance_variable(subject.instance_variable_name)
739
- original_attributes.delete(subject)
740
857
  end
741
858
  end
742
859
 
@@ -762,7 +879,10 @@ module DataMapper
762
879
  #
763
880
  # @api private
764
881
  def eager_load(properties)
765
- unless properties.empty? || key.nil?
882
+ unless properties.empty? || key.nil? || collection.nil?
883
+ # set an initial value to prevent recursive lazy loads
884
+ properties.each { |property| property.set!(self, nil) }
885
+
766
886
  collection.reload(:fields => properties)
767
887
  end
768
888
 
@@ -794,7 +914,10 @@ module DataMapper
794
914
  parent_relationships = []
795
915
 
796
916
  relationships.each_value do |relationship|
797
- next unless relationship.respond_to?(:resource_for) && relationship.loaded?(self) && relationship.get!(self)
917
+ next unless relationship.respond_to?(:resource_for)
918
+ set_default_value(relationship)
919
+ next unless relationship.loaded?(self) && relationship.get!(self)
920
+
798
921
  parent_relationships << relationship
799
922
  end
800
923
 
@@ -811,7 +934,10 @@ module DataMapper
811
934
  child_relationships = []
812
935
 
813
936
  relationships.each_value do |relationship|
814
- next unless relationship.respond_to?(:collection_for) && relationship.loaded?(self) && relationship.get!(self)
937
+ next unless relationship.respond_to?(:collection_for)
938
+ set_default_value(relationship)
939
+ next unless relationship.loaded?(self)
940
+
815
941
  child_relationships << relationship
816
942
  end
817
943
 
@@ -823,13 +949,13 @@ module DataMapper
823
949
  end
824
950
 
825
951
  # @api private
826
- def parent_resources
952
+ def parent_associations
827
953
  parent_relationships.map { |relationship| relationship.get!(self) }
828
954
  end
829
955
 
830
956
  # @api private
831
- def child_collections
832
- child_relationships.map { |relationship| relationship.get!(self) }
957
+ def child_associations
958
+ child_relationships.map { |relationship| relationship.get_collection(self) }
833
959
  end
834
960
 
835
961
  # Creates the resource with default values
@@ -850,26 +976,26 @@ module DataMapper
850
976
  #
851
977
  # @api private
852
978
  def _create
853
- # Can't create a resource that is not dirty and doesn't have serial keys
854
- return false if new? && clean?
979
+ self.persisted_state = persisted_state.commit
980
+ true
981
+ end
855
982
 
856
- # set defaults for new resource
857
- properties.each do |property|
858
- unless property.serial? || property.loaded?(self)
859
- property.set(self, property.default_for(self))
860
- end
983
+ # This method executes the hooks before and after resource creation
984
+ #
985
+ # @return [Boolean]
986
+ #
987
+ # @see Resource#_create
988
+ #
989
+ # @api private
990
+ def create_with_hooks
991
+ catch :halt do
992
+ before_save_hook
993
+ before_create_hook
994
+ retval = _create
995
+ after_create_hook
996
+ after_save_hook
997
+ retval
861
998
  end
862
-
863
- @_repository = repository
864
- @_repository.create([ self ])
865
-
866
- @_saved = true
867
-
868
- original_attributes.clear
869
-
870
- add_to_identity_map
871
-
872
- true
873
999
  end
874
1000
 
875
1001
  # Updates resource state
@@ -883,33 +1009,39 @@ module DataMapper
883
1009
  #
884
1010
  # @api private
885
1011
  def _update
886
- original_attributes = self.original_attributes
887
-
888
- if original_attributes.empty?
889
- true
890
- elsif original_attributes.any? { |property, _value| !property.valid?(property.get!(self)) }
891
- false
892
- else
893
- # remove from the identity map
894
- remove_from_identity_map
895
-
896
- repository.update(dirty_attributes, collection_for_self)
897
-
898
- original_attributes.clear
899
-
900
- # remove the cached key in case it is updated
901
- remove_instance_variable(:@_key)
902
-
903
- add_to_identity_map
1012
+ self.persisted_state = persisted_state.commit if valid_attributes?
1013
+ clean?
1014
+ end
904
1015
 
905
- true
1016
+ # This method executes the hooks before and after resource updating
1017
+ #
1018
+ # @return [Boolean]
1019
+ #
1020
+ # @see Resource#_update
1021
+ #
1022
+ # @api private
1023
+ def update_with_hooks
1024
+ catch :halt do
1025
+ before_save_hook
1026
+ before_update_hook
1027
+ retval = _update
1028
+ after_update_hook
1029
+ after_save_hook
1030
+ retval
906
1031
  end
907
1032
  end
908
1033
 
909
1034
  # @api private
910
- def _save(safe)
1035
+ def _destroy(execute_hooks = true)
1036
+ deleted = persisted_state.delete
1037
+ self.persisted_state = deleted.commit
1038
+ true
1039
+ end
1040
+
1041
+ # @api private
1042
+ def _save(execute_hooks = true)
911
1043
  run_once(true) do
912
- save_parents(safe) && save_self(safe) && save_children(safe)
1044
+ save_parents(execute_hooks) && save_self(execute_hooks) && save_children(execute_hooks)
913
1045
  end
914
1046
  end
915
1047
 
@@ -919,10 +1051,13 @@ module DataMapper
919
1051
  # true if the resource was successfully saved
920
1052
  #
921
1053
  # @api semipublic
922
- def save_self(safe = true)
1054
+ def save_self(execute_hooks = true)
1055
+ # short-circuit if the resource is not dirty
1056
+ return saved? unless dirty_self?
1057
+
923
1058
  new_resource = new?
924
- if safe
925
- new_resource ? create_hook : update_hook
1059
+ if execute_hooks
1060
+ new_resource ? create_with_hooks : update_with_hooks
926
1061
  else
927
1062
  new_resource ? _create : _update
928
1063
  end
@@ -934,15 +1069,15 @@ module DataMapper
934
1069
  # true if the parents were successfully saved
935
1070
  #
936
1071
  # @api private
937
- def save_parents(safe)
1072
+ def save_parents(execute_hooks)
938
1073
  run_once(true) do
939
- parent_relationships.all? do |relationship|
1074
+ parent_relationships.map do |relationship|
940
1075
  parent = relationship.get!(self)
941
1076
 
942
- if parent.__send__(:save_parents, safe) && parent.__send__(:save_self, safe)
1077
+ if parent.__send__(:save_parents, execute_hooks) && parent.__send__(:save_self, execute_hooks)
943
1078
  relationship.set(self, parent) # set the FK values
944
1079
  end
945
- end
1080
+ end.all?
946
1081
  end
947
1082
  end
948
1083
 
@@ -952,10 +1087,10 @@ module DataMapper
952
1087
  # true if the children were successfully saved
953
1088
  #
954
1089
  # @api private
955
- def save_children(safe)
956
- child_collections.all? do |collection|
957
- collection.send(safe ? :save : :save!)
958
- end
1090
+ def save_children(execute_hooks)
1091
+ child_associations.map do |association|
1092
+ association.__send__(execute_hooks ? :save : :save!)
1093
+ end.all?
959
1094
  end
960
1095
 
961
1096
  # Checks if the resource has unsaved changes
@@ -963,7 +1098,7 @@ module DataMapper
963
1098
  # @return [Boolean]
964
1099
  # true if the resource has unsaged changes
965
1100
  #
966
- # @api private
1101
+ # @api semipublic
967
1102
  def dirty_self?
968
1103
  if original_attributes.any?
969
1104
  true
@@ -982,8 +1117,8 @@ module DataMapper
982
1117
  # @api private
983
1118
  def dirty_parents?
984
1119
  run_once(false) do
985
- parent_resources.any? do |parent|
986
- parent.__send__(:dirty_self?) || parent.__send__(:dirty_parents?)
1120
+ parent_associations.any? do |association|
1121
+ association.__send__(:dirty_self?) || association.__send__(:dirty_parents?)
987
1122
  end
988
1123
  end
989
1124
  end
@@ -998,7 +1133,7 @@ module DataMapper
998
1133
  #
999
1134
  # @api private
1000
1135
  def dirty_children?
1001
- child_collections.any? { |children| children.dirty? }
1136
+ child_associations.any? { |association| association.dirty? }
1002
1137
  end
1003
1138
 
1004
1139
  # Return true if +other+'s is equivalent or equal to +self+'s
@@ -1013,17 +1148,46 @@ module DataMapper
1013
1148
  #
1014
1149
  # @api private
1015
1150
  def cmp?(other, operator)
1016
- return false unless key.send(operator, other.key)
1017
- return true if repository.send(operator, other.repository) && clean? && other.clean?
1151
+ return false unless repository.send(operator, other.repository) &&
1152
+ key.send(operator, other.key)
1018
1153
 
1019
- # get all the loaded and non-loaded properties that are not keys,
1020
- # since the key comparison was performed earlier
1021
- loaded, not_loaded = properties.select { |property| !property.key? }.partition do |property|
1022
- property.loaded?(self) && property.loaded?(other)
1154
+ if saved? && other.saved?
1155
+ # if dirty attributes match then they are the same resource
1156
+ dirty_attributes == other.dirty_attributes
1157
+ else
1158
+ # compare properties for unsaved resources
1159
+ properties.all? do |property|
1160
+ __send__(property.name).send(operator, other.__send__(property.name))
1161
+ end
1023
1162
  end
1163
+ end
1024
1164
 
1025
- # check all loaded properties, and then all unloaded properties
1026
- (loaded + not_loaded).all? { |property| property.get(self).send(operator, property.get(other)) }
1165
+ # @api private
1166
+ def set_default_value(subject)
1167
+ return unless persisted_state.respond_to?(:set_default_value, true)
1168
+ persisted_state.__send__(:set_default_value, subject)
1169
+ end
1170
+
1171
+ # @api private
1172
+ def valid_attributes?
1173
+ original_attributes.each_key do |property|
1174
+ return false if property.kind_of?(Property) && !property.valid?(property.get!(self))
1175
+ end
1176
+ true
1177
+ end
1178
+
1179
+ # Execute all the queued up hooks for a given type and name
1180
+ #
1181
+ # @param [Symbol] type
1182
+ # the type of hook to execute (before or after)
1183
+ # @param [Symbol] name
1184
+ # the name of the hook to execute
1185
+ #
1186
+ # @return [undefined]
1187
+ #
1188
+ # @api private
1189
+ def execute_hooks_for(type, name)
1190
+ model.hooks[name][type].each { |hook| hook.call(self) }
1027
1191
  end
1028
1192
 
1029
1193
  # Raises an exception if #update is performed on a dirty resource
@@ -1039,7 +1203,7 @@ module DataMapper
1039
1203
  # @api private
1040
1204
  def assert_update_clean_only(method)
1041
1205
  if dirty?
1042
- raise UpdateConflictError, "#{model}##{method} cannot be called on a dirty resource"
1206
+ raise UpdateConflictError, "#{model}##{method} cannot be called on a #{new? ? 'new' : 'dirty'} resource"
1043
1207
  end
1044
1208
  end
1045
1209
 
@@ -1060,6 +1224,25 @@ module DataMapper
1060
1224
  end
1061
1225
  end
1062
1226
 
1227
+ # Raises an exception if #save returns false
1228
+ #
1229
+ # @param [Symbol] method
1230
+ # the name of the method to use in the exception
1231
+ # @param [Boolean] save_result
1232
+ # the result of the #save call
1233
+ #
1234
+ # @return [undefined]
1235
+ #
1236
+ # @raise [SaveFailureError]
1237
+ # raise if the resource was not saved
1238
+ #
1239
+ # @api private
1240
+ def assert_save_successful(method, save_retval)
1241
+ if save_retval != true && raise_on_save_failure
1242
+ raise SaveFailureError, "#{model}##{method} returned #{save_retval.inspect}, #{model} was not saved"
1243
+ end
1244
+ end
1245
+
1063
1246
  # Prevent a method from being in the stack more than once
1064
1247
  #
1065
1248
  # The purpose of this method is to prevent SystemStackError from