datamapper-dm-core 0.9.11 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -39
  5. data/Manifest.txt +67 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +16 -15
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/dm-core.gemspec +11 -15
  12. data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
  13. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  14. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  15. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  16. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  19. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  20. data/lib/dm-core/adapters.rb +135 -16
  21. data/lib/dm-core/associations/many_to_many.rb +372 -90
  22. data/lib/dm-core/associations/many_to_one.rb +220 -73
  23. data/lib/dm-core/associations/one_to_many.rb +319 -255
  24. data/lib/dm-core/associations/one_to_one.rb +66 -53
  25. data/lib/dm-core/associations/relationship.rb +560 -158
  26. data/lib/dm-core/collection.rb +1104 -381
  27. data/lib/dm-core/core_ext/kernel.rb +12 -0
  28. data/lib/dm-core/core_ext/symbol.rb +10 -0
  29. data/lib/dm-core/identity_map.rb +4 -34
  30. data/lib/dm-core/migrations.rb +1283 -0
  31. data/lib/dm-core/model/descendant_set.rb +81 -0
  32. data/lib/dm-core/model/hook.rb +45 -0
  33. data/lib/dm-core/model/is.rb +32 -0
  34. data/lib/dm-core/model/property.rb +248 -0
  35. data/lib/dm-core/model/relationship.rb +335 -0
  36. data/lib/dm-core/model/scope.rb +90 -0
  37. data/lib/dm-core/model.rb +570 -369
  38. data/lib/dm-core/property.rb +753 -280
  39. data/lib/dm-core/property_set.rb +141 -98
  40. data/lib/dm-core/query/conditions/comparison.rb +814 -0
  41. data/lib/dm-core/query/conditions/operation.rb +247 -0
  42. data/lib/dm-core/query/direction.rb +43 -0
  43. data/lib/dm-core/query/operator.rb +42 -0
  44. data/lib/dm-core/query/path.rb +102 -0
  45. data/lib/dm-core/query/sort.rb +45 -0
  46. data/lib/dm-core/query.rb +974 -492
  47. data/lib/dm-core/repository.rb +147 -107
  48. data/lib/dm-core/resource.rb +644 -429
  49. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  50. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  51. data/lib/dm-core/support/chainable.rb +20 -0
  52. data/lib/dm-core/support/deprecate.rb +12 -0
  53. data/lib/dm-core/support/equalizer.rb +23 -0
  54. data/lib/dm-core/support/logger.rb +13 -0
  55. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  56. data/lib/dm-core/transaction.rb +333 -92
  57. data/lib/dm-core/type.rb +98 -60
  58. data/lib/dm-core/types/boolean.rb +1 -1
  59. data/lib/dm-core/types/discriminator.rb +34 -20
  60. data/lib/dm-core/types/object.rb +7 -4
  61. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  62. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  63. data/lib/dm-core/types/serial.rb +3 -3
  64. data/lib/dm-core/types/text.rb +3 -4
  65. data/lib/dm-core/version.rb +1 -1
  66. data/lib/dm-core.rb +106 -110
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/model/relationship_spec.rb +924 -0
  80. data/spec/public/model_spec.rb +159 -0
  81. data/spec/public/property_spec.rb +829 -0
  82. data/spec/public/resource_spec.rb +71 -0
  83. data/spec/public/sel_spec.rb +44 -0
  84. data/spec/public/setup_spec.rb +145 -0
  85. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  86. data/spec/public/shared/collection_shared_spec.rb +1723 -0
  87. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  88. data/spec/public/shared/resource_shared_spec.rb +924 -0
  89. data/spec/public/shared/sel_shared_spec.rb +112 -0
  90. data/spec/public/transaction_spec.rb +129 -0
  91. data/spec/public/types/discriminator_spec.rb +130 -0
  92. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  93. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  94. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  95. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  96. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  97. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  99. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  100. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  101. data/spec/semipublic/associations_spec.rb +177 -0
  102. data/spec/semipublic/collection_spec.rb +142 -0
  103. data/spec/semipublic/property_spec.rb +61 -0
  104. data/spec/semipublic/query/conditions_spec.rb +528 -0
  105. data/spec/semipublic/query/path_spec.rb +443 -0
  106. data/spec/semipublic/query_spec.rb +2626 -0
  107. data/spec/semipublic/resource_spec.rb +47 -0
  108. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  109. data/spec/spec.opts +3 -1
  110. data/spec/spec_helper.rb +80 -57
  111. data/tasks/ci.rb +19 -31
  112. data/tasks/dm.rb +43 -48
  113. data/tasks/doc.rb +8 -11
  114. data/tasks/gemspec.rb +5 -5
  115. data/tasks/hoe.rb +15 -16
  116. data/tasks/install.rb +8 -10
  117. metadata +72 -93
  118. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  119. data/lib/dm-core/associations.rb +0 -207
  120. data/lib/dm-core/auto_migrations.rb +0 -105
  121. data/lib/dm-core/dependency_queue.rb +0 -32
  122. data/lib/dm-core/hook.rb +0 -11
  123. data/lib/dm-core/is.rb +0 -16
  124. data/lib/dm-core/logger.rb +0 -232
  125. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  126. data/lib/dm-core/migrator.rb +0 -29
  127. data/lib/dm-core/scope.rb +0 -58
  128. data/lib/dm-core/support/array.rb +0 -13
  129. data/lib/dm-core/support/assertions.rb +0 -8
  130. data/lib/dm-core/support/errors.rb +0 -23
  131. data/lib/dm-core/support/kernel.rb +0 -11
  132. data/lib/dm-core/support/symbol.rb +0 -41
  133. data/lib/dm-core/support.rb +0 -7
  134. data/lib/dm-core/type_map.rb +0 -80
  135. data/lib/dm-core/types.rb +0 -19
  136. data/script/all +0 -4
  137. data/spec/integration/association_spec.rb +0 -1382
  138. data/spec/integration/association_through_spec.rb +0 -203
  139. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  140. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  141. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  142. data/spec/integration/auto_migrations_spec.rb +0 -413
  143. data/spec/integration/collection_spec.rb +0 -1073
  144. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  145. data/spec/integration/dependency_queue_spec.rb +0 -46
  146. data/spec/integration/model_spec.rb +0 -197
  147. data/spec/integration/mysql_adapter_spec.rb +0 -85
  148. data/spec/integration/postgres_adapter_spec.rb +0 -731
  149. data/spec/integration/property_spec.rb +0 -253
  150. data/spec/integration/query_spec.rb +0 -514
  151. data/spec/integration/repository_spec.rb +0 -61
  152. data/spec/integration/resource_spec.rb +0 -513
  153. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  154. data/spec/integration/sti_spec.rb +0 -273
  155. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  156. data/spec/integration/transaction_spec.rb +0 -75
  157. data/spec/integration/type_spec.rb +0 -275
  158. data/spec/lib/logging_helper.rb +0 -18
  159. data/spec/lib/mock_adapter.rb +0 -27
  160. data/spec/lib/model_loader.rb +0 -100
  161. data/spec/lib/publicize_methods.rb +0 -28
  162. data/spec/models/content.rb +0 -16
  163. data/spec/models/vehicles.rb +0 -34
  164. data/spec/models/zoo.rb +0 -48
  165. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  166. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  167. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  168. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  169. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  170. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  171. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  172. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  173. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  174. data/spec/unit/associations/relationship_spec.rb +0 -71
  175. data/spec/unit/associations_spec.rb +0 -242
  176. data/spec/unit/auto_migrations_spec.rb +0 -111
  177. data/spec/unit/collection_spec.rb +0 -182
  178. data/spec/unit/data_mapper_spec.rb +0 -35
  179. data/spec/unit/identity_map_spec.rb +0 -126
  180. data/spec/unit/is_spec.rb +0 -80
  181. data/spec/unit/migrator_spec.rb +0 -33
  182. data/spec/unit/model_spec.rb +0 -321
  183. data/spec/unit/naming_conventions_spec.rb +0 -36
  184. data/spec/unit/property_set_spec.rb +0 -90
  185. data/spec/unit/property_spec.rb +0 -753
  186. data/spec/unit/query_spec.rb +0 -571
  187. data/spec/unit/repository_spec.rb +0 -93
  188. data/spec/unit/resource_spec.rb +0 -649
  189. data/spec/unit/scope_spec.rb +0 -142
  190. data/spec/unit/transaction_spec.rb +0 -493
  191. data/spec/unit/type_map_spec.rb +0 -114
  192. data/spec/unit/type_spec.rb +0 -119
@@ -1,147 +1,429 @@
1
- require File.join(File.dirname(__FILE__), "one_to_many")
2
1
  module DataMapper
3
2
  module Associations
4
- module ManyToMany
5
- extend Assertions
3
+ module ManyToMany #:nodoc:
4
+ class Relationship < Associations::OneToMany::Relationship
5
+ extend Chainable
6
6
 
7
- # Setup many to many relationship between two models
8
- # -
9
- # @api private
10
- def self.setup(name, model, options = {})
11
- assert_kind_of 'name', name, Symbol
12
- assert_kind_of 'model', model, Model
13
- assert_kind_of 'options', options, Hash
7
+ OPTIONS = superclass::OPTIONS.dup << :through << :via
14
8
 
15
- repository_name = model.repository.name
9
+ # Returns a set of keys that identify the target model
10
+ #
11
+ # @return [DataMapper::PropertySet]
12
+ # a set of properties that identify the target model
13
+ #
14
+ # @api semipublic
15
+ def child_key
16
+ return @child_key if defined?(@child_key)
16
17
 
17
- model.class_eval <<-EOS, __FILE__, __LINE__
18
- def #{name}(query = {})
19
- #{name}_association.all(query)
18
+ repository_name = child_repository_name || parent_repository_name
19
+ properties = child_model.properties(repository_name)
20
+
21
+ @child_key = if @child_properties
22
+ child_key = properties.values_at(*@child_properties)
23
+ properties.class.new(child_key).freeze
24
+ else
25
+ properties.key
26
+ end
27
+ end
28
+
29
+ # TODO: document
30
+ # @api semipublic
31
+ alias target_key child_key
32
+
33
+ # Intermediate association for through model
34
+ # relationships
35
+ #
36
+ # Example: for :bugs association in
37
+ #
38
+ # class Software::Engineer
39
+ # include DataMapper::Resource
40
+ #
41
+ # has n, :missing_tests
42
+ # has n, :bugs, :through => :missing_tests
43
+ # end
44
+ #
45
+ # through is :missing_tests
46
+ #
47
+ # TODO: document a case when
48
+ # through option is a model and
49
+ # not an association name
50
+ #
51
+ # @api semipublic
52
+ def through
53
+ return @through if defined?(@through)
54
+
55
+ if options[:through].kind_of?(Associations::Relationship)
56
+ return @through = options[:through]
57
+ end
58
+
59
+ repository_name = source_repository_name
60
+ relationships = source_model.relationships(repository_name)
61
+ name = through_relationship_name
62
+
63
+ @through = relationships[name] ||
64
+ DataMapper.repository(repository_name) do
65
+ source_model.has(min..max, name, through_model, one_to_many_options)
66
+ end
67
+
68
+ @through.child_key
69
+
70
+ @through
71
+ end
72
+
73
+ # TODO: document
74
+ # @api semipublic
75
+ def via
76
+ return @via if defined?(@via)
77
+
78
+ if options[:via].kind_of?(Associations::Relationship)
79
+ return @via = options[:via]
80
+ end
81
+
82
+ repository_name = through.relative_target_repository_name
83
+ through_model = through.target_model
84
+ relationships = through_model.relationships(repository_name)
85
+ singular_name = name.to_s.singularize.to_sym
86
+
87
+ @via = relationships[options[:via]] ||
88
+ relationships[name] ||
89
+ relationships[singular_name]
90
+
91
+ @via ||= if anonymous_through_model?
92
+ DataMapper.repository(repository_name) do
93
+ through_model.belongs_to(singular_name, target_model, many_to_one_options)
94
+ end
95
+ else
96
+ raise UnknownRelationshipError, "No relationships named #{name} or #{singular_name} in #{through_model}"
20
97
  end
21
98
 
22
- def #{name}=(children)
23
- #{name}_association.replace(children)
99
+ @via.child_key
100
+
101
+ @via
102
+ end
103
+
104
+ # TODO: document
105
+ # @api semipublic
106
+ def links
107
+ return @links if defined?(@links)
108
+
109
+ @links = []
110
+ links = [ through, via ]
111
+
112
+ while relationship = links.shift
113
+ if relationship.respond_to?(:links)
114
+ links.unshift(*relationship.links)
115
+ else
116
+ @links << relationship
117
+ end
24
118
  end
25
119
 
26
- private
120
+ @links.freeze
121
+ end
122
+
123
+ # TODO: document
124
+ # @api private
125
+ def source_scope(source)
126
+ { through.inverse => source }
127
+ end
27
128
 
28
- def #{name}_association
29
- @#{name}_association ||= begin
30
- unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
31
- raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
129
+ # TODO: document
130
+ # @api private
131
+ def query
132
+ # TODO: consider making this a query_for method, so that ManyToMany::Relationship#query only
133
+ # returns the query supplied in the definition
134
+ @many_to_many_query ||= super.merge(:links => links).freeze
135
+ end
136
+
137
+ # Eager load the collection using the source as a base
138
+ #
139
+ # @param [Resource, Collection] source
140
+ # the source to query with
141
+ # @param [Query, Hash] other_query
142
+ # optional query to restrict the collection
143
+ #
144
+ # @return [ManyToMany::Collection]
145
+ # the loaded collection for the source
146
+ #
147
+ # @api private
148
+ def eager_load(source, other_query = nil)
149
+ # FIXME: enable SEL for m:m relationships
150
+ source.model.all(query_for(source, other_query))
151
+ end
152
+
153
+ private
154
+
155
+ # TODO: document
156
+ # @api private
157
+ def through_model
158
+ namespace, name = through_model_namespace_name
159
+
160
+ if namespace.const_defined?(name)
161
+ namespace.const_get(name)
162
+ else
163
+ model = Model.new do
164
+ # all properties added to the anonymous through model are keys by default
165
+ def property(name, type, options = {})
166
+ options[:key] = true unless options.key?(:key)
167
+ options.delete(:index)
168
+ super
32
169
  end
33
- association = Proxy.new(relationship, self)
34
- parent_associations << association
35
- association
36
170
  end
171
+
172
+ namespace.const_set(name, model)
37
173
  end
38
- EOS
174
+ end
39
175
 
40
- opts = options.dup
41
- opts.delete(:through)
42
- opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
43
- opts[:parent_model] = model
44
- opts[:repository_name] = repository_name
45
- opts[:remote_relationship_name] ||= opts.delete(:remote_name) || Extlib::Inflection.tableize(opts[:child_model])
46
- opts[:parent_key] = opts[:parent_key]
47
- opts[:child_key] = opts[:child_key]
48
- opts[:mutable] = true
176
+ # TODO: document
177
+ # @api private
178
+ def through_model_namespace_name
179
+ target_parts = target_model.base_model.name.split('::')
180
+ source_parts = source_model.base_model.name.split('::')
49
181
 
50
- names = [ opts[:child_model], opts[:parent_model].name ].sort
51
- model_name = names.join.gsub("::", "")
52
- storage_name = Extlib::Inflection.tableize(Extlib::Inflection.pluralize(names[0]) + names[1])
182
+ name = [ target_parts.pop, source_parts.pop ].sort.join
53
183
 
54
- opts[:near_relationship_name] = Extlib::Inflection.tableize(model_name).to_sym
184
+ namespace = Object
55
185
 
56
- model.has(model.n, opts[:near_relationship_name])
186
+ # find the common namespace between the target_model and source_model
187
+ target_parts.zip(source_parts) do |target_part, source_part|
188
+ break if target_part != source_part
189
+ namespace = namespace.const_get(target_part)
190
+ end
57
191
 
58
- relationship = model.relationships(repository_name)[name] = RelationshipChain.new(opts)
192
+ return namespace, name
193
+ end
59
194
 
60
- unless Object.const_defined?(model_name)
61
- model = DataMapper::Model.new(storage_name)
195
+ # TODO: document
196
+ # @api private
197
+ def through_relationship_name
198
+ if anonymous_through_model?
199
+ namespace = through_model_namespace_name.first
200
+ relationship_name = Extlib::Inflection.underscore(through_model.name.sub(/\A#{namespace.name}::/, '')).tr('/', '_')
201
+ relationship_name.pluralize.to_sym
202
+ else
203
+ options[:through]
204
+ end
205
+ end
62
206
 
63
- model.class_eval <<-EOS, __FILE__, __LINE__
64
- def self.name; #{model_name.inspect} end
65
- def self.default_repository_name; #{repository_name.inspect} end
66
- def self.many_to_many; true end
67
- EOS
207
+ # Check if the :through association uses an anonymous model
208
+ #
209
+ # An anonymous model means that DataMapper creates the model
210
+ # in-memory, and sets the relationships to join the source
211
+ # and the target model.
212
+ #
213
+ # @return [Boolean]
214
+ # true if the through model is anonymous
215
+ #
216
+ # @api private
217
+ def anonymous_through_model?
218
+ options[:through] == Resource
219
+ end
68
220
 
69
- names.each do |n|
70
- model.belongs_to(Extlib::Inflection.underscore(n).gsub('/', '_').to_sym)
221
+ # TODO: document
222
+ # @api semipublic
223
+ chainable do
224
+ def many_to_one_options
225
+ { :parent_key => target_key.map { |property| property.name } }
71
226
  end
227
+ end
72
228
 
73
- Object.const_set(model_name, model)
229
+ # TODO: document
230
+ # @api semipublic
231
+ chainable do
232
+ def one_to_many_options
233
+ { :parent_key => source_key.map { |property| property.name } }
234
+ end
74
235
  end
75
236
 
76
- relationship
77
- end
237
+ # Returns the inverse relationship class
238
+ #
239
+ # @api private
240
+ def inverse_class
241
+ self.class
242
+ end
78
243
 
79
- class Proxy < DataMapper::Associations::OneToMany::Proxy
80
- def delete(resource)
81
- through = near_association.get(*(@parent.key + resource.key))
82
- near_association.delete(through)
83
- orphan_resource(super)
244
+ # TODO: document
245
+ # @api private
246
+ def invert
247
+ inverse_class.new(inverse_name, parent_model, child_model, inverted_options)
84
248
  end
85
249
 
86
- def clear
87
- near_association.clear
88
- super
250
+ # TODO: document
251
+ # @api private
252
+ def inverted_options
253
+ links = self.links.dup
254
+ through = links.pop.inverse
255
+
256
+ links.reverse_each do |relationship|
257
+ inverse = relationship.inverse
258
+
259
+ through = self.class.new(
260
+ inverse.name,
261
+ inverse.child_model,
262
+ inverse.parent_model,
263
+ inverse.options.merge(:through => through)
264
+ )
265
+ end
266
+
267
+ options.only(*OPTIONS - [ :min, :max ]).update(
268
+ :through => through,
269
+ :child_key => options[:parent_key],
270
+ :parent_key => options[:child_key],
271
+ :inverse => self
272
+ )
273
+ end
274
+
275
+ # Loads association targets and sets resulting value on
276
+ # given source resource
277
+ #
278
+ # @param [Resource] source
279
+ # the source resource for the association
280
+ #
281
+ # @return [undefined]
282
+ #
283
+ # @api private
284
+ def lazy_load(source)
285
+ # FIXME: delegate to super once SEL is enabled
286
+ set!(source, collection_for(source))
89
287
  end
90
288
 
289
+ # Returns collection class used by this type of
290
+ # relationship
291
+ #
292
+ # @api private
293
+ def collection_class
294
+ ManyToMany::Collection
295
+ end
296
+ end # class Relationship
297
+
298
+ class Collection < Associations::OneToMany::Collection
299
+ # Remove every Resource in the m:m Collection from the repository
300
+ #
301
+ # This performs a deletion of each Resource in the Collection from
302
+ # the repository and clears the Collection.
303
+ #
304
+ # @return [Boolean]
305
+ # true if the resources were successfully destroyed
306
+ #
307
+ # @api public
91
308
  def destroy
92
- near_association.destroy
309
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
310
+
311
+ # make sure the records are loaded so they can be found when
312
+ # the intermediaries are removed
313
+ lazy_load
314
+
315
+ unless intermediaries.destroy
316
+ return false
317
+ end
318
+
93
319
  super
94
320
  end
95
321
 
96
- def save
322
+ # Remove every Resource in the m:m Collection from the repository, bypassing validation
323
+ #
324
+ # This performs a deletion of each Resource in the Collection from
325
+ # the repository and clears the Collection while skipping
326
+ # validation.
327
+ #
328
+ # @return [Boolean]
329
+ # true if the resources were successfully destroyed
330
+ #
331
+ # @api public
332
+ def destroy!
333
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
334
+
335
+ # make sure the records are loaded so they can be found when
336
+ # the intermediaries are removed
337
+ lazy_load
338
+
339
+ unless intermediaries.destroy!
340
+ return false
341
+ end
342
+
343
+ super
97
344
  end
98
345
 
99
346
  private
100
347
 
101
- def new_child(attributes)
102
- remote_relationship.parent_model.new(attributes)
348
+ # TODO: document
349
+ # @api private
350
+ def _create(safe, attributes)
351
+ if via.respond_to?(:resource_for)
352
+ resource = super
353
+ if create_intermediary(safe, via => resource)
354
+ resource
355
+ end
356
+ else
357
+ if intermediary = create_intermediary(safe)
358
+ super(safe, attributes.merge(via.inverse => intermediary))
359
+ end
360
+ end
103
361
  end
104
362
 
105
- def relate_resource(resource)
106
- assert_mutable
107
- add_default_association_values(resource)
108
- @orphans.delete(resource)
363
+ # TODO: document
364
+ # @api private
365
+ def _save(safe)
366
+ # delete only intermediaries linked to the removed targets
367
+ unless @removed.empty? || intermediaries(@removed).send(safe ? :destroy : :destroy!)
368
+ return false
369
+ end
109
370
 
110
- # TODO: fix this so it does not automatically save on append, if possible
111
- resource.save if resource.new_record?
112
- through_resource = @relationship.child_model.new
113
- @relationship.child_key.zip(@relationship.parent_key) do |child_key,parent_key|
114
- through_resource.send("#{child_key.name}=", parent_key.get(@parent))
371
+ if via.respond_to?(:resource_for)
372
+ super
373
+ loaded_entries.all? { |resource| create_intermediary(safe, via => resource) }
374
+ else
375
+ if intermediary = create_intermediary(safe)
376
+ inverse = via.inverse
377
+ loaded_entries.map { |resource| inverse.set(resource, intermediary) }
378
+ end
379
+
380
+ super
115
381
  end
116
- remote_relationship.child_key.zip(remote_relationship.parent_key) do |child_key,parent_key|
117
- through_resource.send("#{child_key.name}=", parent_key.get(resource))
382
+ end
383
+
384
+ # TODO: document
385
+ # @api private
386
+ def intermediaries(targets = self)
387
+ intermediaries = if through.loaded?(source)
388
+ through.get!(source)
389
+ else
390
+ through.set!(source, through.collection_for(source))
118
391
  end
119
- near_association << through_resource
120
392
 
121
- resource
393
+ intermediaries.all(via => targets)
122
394
  end
123
395
 
124
- def orphan_resource(resource)
125
- assert_mutable
126
- @orphans << resource
127
- resource
128
- end
396
+ # TODO: document
397
+ # @api private
398
+ def create_intermediary(safe, attributes = {})
399
+ collection = intermediaries
400
+
401
+ return unless collection.send(safe ? :save : :save!)
402
+
403
+ intermediary = collection.first(attributes) ||
404
+ collection.send(safe ? :create : :create!, attributes)
129
405
 
130
- def assert_mutable
406
+ return intermediary if intermediary.saved?
131
407
  end
132
408
 
133
- def remote_relationship
134
- @remote_relationship ||= @relationship.send(:remote_relationship)
409
+ # TODO: document
410
+ # @api private
411
+ def through
412
+ relationship.through
135
413
  end
136
414
 
137
- def near_association
138
- @near_association ||= @parent.send(near_relationship_name)
415
+ # TODO: document
416
+ # @api private
417
+ def via
418
+ relationship.via
139
419
  end
140
420
 
141
- def near_relationship_name
142
- @near_relationship_name ||= @relationship.send(:instance_variable_get, :@near_relationship_name)
421
+ # TODO: document
422
+ # @api private
423
+ def inverse_set(*)
424
+ # do nothing
143
425
  end
144
- end # class Proxy
426
+ end # class Collection
145
427
  end # module ManyToMany
146
428
  end # module Associations
147
429
  end # module DataMapper