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,228 +1,630 @@
1
1
  module DataMapper
2
2
  module Associations
3
+ # Base class for relationships. Each type of relationship
4
+ # (1 to 1, 1 to n, n to m) implements a subclass of this class
5
+ # with methods like get and set overridden.
3
6
  class Relationship
4
- include Assertions
7
+ include Extlib::Assertions
8
+
9
+ OPTIONS = [ :child_repository_name, :parent_repository_name, :child_key, :parent_key, :min, :max, :inverse ].to_set
10
+
11
+ # Relationship name
12
+ #
13
+ # @example for :parent association in
14
+ #
15
+ # class VersionControl::Commit
16
+ # # ...
17
+ #
18
+ # belongs_to :parent
19
+ # end
20
+ #
21
+ # name is :parent
22
+ #
23
+ # @api semipublic
24
+ attr_reader :name
25
+
26
+ # Options used to set up association of this relationship
27
+ #
28
+ # @example for :author association in
29
+ #
30
+ # class VersionControl::Commit
31
+ # # ...
32
+ #
33
+ # belongs_to :author, :model => 'Person'
34
+ # end
35
+ #
36
+ # options is a hash with a single key, :model
37
+ #
38
+ # @api semipublic
39
+ attr_reader :options
40
+
41
+ # ivar used to store collection of child options in source
42
+ #
43
+ # @example for :commits association in
44
+ #
45
+ # class VersionControl::Branch
46
+ # # ...
47
+ #
48
+ # has n, :commits
49
+ # end
50
+ #
51
+ # instance variable name for source will be @commits
52
+ #
53
+ # @api semipublic
54
+ attr_reader :instance_variable_name
55
+
56
+ # Repository from where child objects are loaded
57
+ #
58
+ # @api semipublic
59
+ attr_reader :child_repository_name
60
+
61
+ # Repository from where parent objects are loaded
62
+ #
63
+ # @api semipublic
64
+ attr_reader :parent_repository_name
65
+
66
+ # Minimum number of child objects for relationship
67
+ #
68
+ # @example for :cores association in
69
+ #
70
+ # class CPU::Multicore
71
+ # # ...
72
+ #
73
+ # has 2..n, :cores
74
+ # end
75
+ #
76
+ # minimum is 2
77
+ #
78
+ # @api semipublic
79
+ attr_reader :min
80
+
81
+ # Maximum number of child objects for
82
+ # relationship
83
+ #
84
+ # @example for :fouls association in
85
+ #
86
+ # class Basketball::Player
87
+ # # ...
88
+ #
89
+ # has 0..5, :fouls
90
+ # end
91
+ #
92
+ # maximum is 5
93
+ #
94
+ # @api semipublic
95
+ attr_reader :max
96
+
97
+ # Returns query options for relationship.
98
+ #
99
+ # For this base class, always returns query options
100
+ # has been initialized with.
101
+ # Overriden in subclasses.
102
+ #
103
+ # @api private
104
+ attr_reader :query
105
+
106
+ # Returns a hash of conditions that scopes query that fetches
107
+ # target object
108
+ #
109
+ # @return [Hash]
110
+ # Hash of conditions that scopes query
111
+ #
112
+ # @api private
113
+ def source_scope(source)
114
+ { inverse => source }
115
+ end
5
116
 
6
- OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :through ]
117
+ # Creates and returns Query instance that fetches
118
+ # target resource(s) (ex.: articles) for given target resource (ex.: author)
119
+ #
120
+ # @api semipublic
121
+ def query_for(source, other_query = nil)
122
+ repository_name = relative_target_repository_name_for(source)
123
+
124
+ DataMapper.repository(repository_name).scope do
125
+ query = target_model.query.dup
126
+ query.update(self.query)
127
+ query.update(source_scope(source))
128
+ query.update(other_query) if other_query
129
+ query.update(:fields => query.fields | target_key)
130
+ end
131
+ end
7
132
 
133
+ # Returns model class used by child side of the relationship
134
+ #
135
+ # @return [Resource]
136
+ # Model for association child
137
+ #
8
138
  # @api private
9
- attr_reader :name, :options, :query
139
+ def child_model
140
+ @child_model ||= (@parent_model || Object).find_const(child_model_name)
141
+ rescue NameError
142
+ raise NameError, "Cannot find the child_model #{child_model_name} for #{parent_model_name} in #{name}"
143
+ end
10
144
 
145
+ # TODO: document
11
146
  # @api private
12
- def child_key
13
- @child_key ||= begin
14
- child_key = nil
15
- child_model.repository.scope do |r|
16
- model_properties = child_model.properties(r.name)
17
-
18
- child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
19
- # TODO: use something similar to DM::NamingConventions to determine the property name
20
- parent_name = Extlib::Inflection.underscore(Extlib::Inflection.demodulize(parent_model.base_model.name))
21
- property_name ||= "#{parent_name}_#{parent_property.name}".to_sym
22
-
23
- if model_properties.has_property?(property_name)
24
- model_properties[property_name]
25
- else
26
- options = {}
27
-
28
- [ :length, :precision, :scale ].each do |option|
29
- options[option] = parent_property.send(option)
30
- end
31
-
32
- # NOTE: hack to make each many to many child_key a true key,
33
- # until I can figure out a better place for this check
34
- if child_model.respond_to?(:many_to_many)
35
- options[:key] = true
36
- end
37
-
38
- child_model.property(property_name, parent_property.primitive, options)
39
- end
40
- end
41
- end
42
- PropertySet.new(child_key)
43
- end
147
+ def child_model?
148
+ child_model
149
+ true
150
+ rescue NameError
151
+ false
44
152
  end
45
153
 
154
+ # TODO: document
46
155
  # @api private
47
- def parent_key
48
- @parent_key ||= begin
49
- parent_key = nil
50
- parent_model.repository.scope do |r|
51
- parent_key = if @parent_properties
52
- parent_model.properties(r.name).slice(*@parent_properties)
53
- else
54
- parent_model.key
55
- end
56
- end
57
- PropertySet.new(parent_key)
156
+ def child_model_name
157
+ @child_model ? child_model.name : @child_model_name
158
+ end
159
+
160
+ # Returns a set of keys that identify the target model
161
+ #
162
+ # @return [PropertySet]
163
+ # a set of properties that identify the target model
164
+ #
165
+ # @api semipublic
166
+ def child_key
167
+ return @child_key if defined?(@child_key)
168
+
169
+ repository_name = child_repository_name || parent_repository_name
170
+ properties = child_model.properties(repository_name)
171
+
172
+ @child_key = if @child_properties
173
+ child_key = properties.values_at(*@child_properties)
174
+ properties.class.new(child_key).freeze
175
+ else
176
+ properties.key
58
177
  end
59
178
  end
60
179
 
180
+ # Access Relationship#child_key directly
181
+ #
182
+ # @api private
183
+ alias relationship_child_key child_key
184
+ private :relationship_child_key
185
+
186
+ # Returns model class used by parent side of the relationship
187
+ #
188
+ # @return [Resource]
189
+ # Class of association parent
190
+ #
61
191
  # @api private
62
192
  def parent_model
63
- return @parent_model if model_defined?(@parent_model)
64
- @parent_model = @child_model.find_const(@parent_model)
193
+ @parent_model ||= (@child_model || Object).find_const(parent_model_name)
65
194
  rescue NameError
66
- raise NameError, "Cannot find the parent_model #{@parent_model} for #{@child_model}"
195
+ raise NameError, "Cannot find the parent_model #{parent_model_name} for #{child_model_name} in #{name}"
67
196
  end
68
197
 
198
+ # TODO: document
69
199
  # @api private
70
- def child_model
71
- return @child_model if model_defined?(@child_model)
72
- @child_model = @parent_model.find_const(@child_model)
200
+ def parent_model?
201
+ parent_model
202
+ true
73
203
  rescue NameError
74
- raise NameError, "Cannot find the child_model #{@child_model} for #{@parent_model}"
204
+ false
75
205
  end
76
206
 
207
+ # TODO: document
77
208
  # @api private
78
- def get_children(parent, options = {}, finder = :all, *args)
79
- parent_value = parent_key.get(parent)
80
- bind_values = [ parent_value ]
209
+ def parent_model_name
210
+ @parent_model ? parent_model.name : @parent_model_name
211
+ end
81
212
 
82
- with_repository(child_model) do |r|
83
- parent_identity_map = parent.repository.identity_map(parent_model)
84
- child_identity_map = r.identity_map(child_model)
213
+ # Returns a set of keys that identify parent model
214
+ #
215
+ # @return [PropertySet]
216
+ # a set of properties that identify parent model
217
+ #
218
+ # @api private
219
+ def parent_key
220
+ return @parent_key if defined?(@parent_key)
85
221
 
86
- query_values = parent_identity_map.keys
87
- query_values.reject! { |k| child_identity_map[k] }
222
+ repository_name = parent_repository_name || child_repository_name
223
+ properties = parent_model.properties(repository_name)
88
224
 
89
- bind_values = query_values unless query_values.empty?
90
- query = child_key.zip(bind_values.transpose).to_hash
225
+ @parent_key = if @parent_properties
226
+ parent_key = properties.values_at(*@parent_properties)
227
+ properties.class.new(parent_key).freeze
228
+ else
229
+ properties.key
230
+ end
231
+ end
91
232
 
92
- collection = child_model.send(finder, *(args.dup << @query.merge(options).merge(query)))
233
+ # Loads and returns "other end" of the association.
234
+ # Must be implemented in subclasses.
235
+ #
236
+ # @api semipublic
237
+ def get(resource, other_query = nil)
238
+ raise NotImplementedError, "#{self.class}#get not implemented"
239
+ end
93
240
 
94
- return collection unless collection.kind_of?(Collection) && collection.any?
241
+ # Gets "other end" of the association directly
242
+ # as @ivar on given resource. Subclasses usually
243
+ # use implementation of this class.
244
+ #
245
+ # @api semipublic
246
+ def get!(resource)
247
+ resource.instance_variable_get(instance_variable_name)
248
+ end
95
249
 
96
- grouped_collection = {}
97
- collection.each do |resource|
98
- child_value = child_key.get(resource)
99
- parent_obj = parent_identity_map[child_value]
100
- grouped_collection[parent_obj] ||= []
101
- grouped_collection[parent_obj] << resource
102
- end
250
+ # Sets value of the "other end" of association
251
+ # on given resource. Must be implemented in subclasses.
252
+ #
253
+ # @api semipublic
254
+ def set(resource, association)
255
+ raise NotImplementedError, "#{self.class}#set not implemented"
256
+ end
103
257
 
104
- association_accessor = "#{self.name}_association"
258
+ # Sets "other end" of the association directly
259
+ # as @ivar on given resource. Subclasses usually
260
+ # use implementation of this class.
261
+ #
262
+ # @api semipublic
263
+ def set!(resource, association)
264
+ resource.instance_variable_set(instance_variable_name, association)
265
+ end
105
266
 
106
- ret = nil
107
- grouped_collection.each do |parent, children|
108
- association = parent.send(association_accessor)
267
+ # Eager load the collection using the source as a base
268
+ #
269
+ # @param [Resource, Collection] source
270
+ # the source to query with
271
+ # @param [Query, Hash] query
272
+ # optional query to restrict the collection
273
+ #
274
+ # @return [Collection]
275
+ # the loaded collection for the source
276
+ #
277
+ # @api private
278
+ def eager_load(source, query = nil)
279
+ target_maps = Hash.new { |h,k| h[k] = [] }
109
280
 
110
- query = collection.query.dup
111
- query.conditions.map! do |operator, property, bind_value|
112
- if operator != :raw && child_key.has_property?(property.name)
113
- bind_value = *children.map { |child| property.get(child) }.uniq
114
- end
115
- [ operator, property, bind_value ]
116
- end
281
+ collection_query = query_for(source, query)
117
282
 
118
- parents_children = Collection.new(query)
119
- children.each { |child| parents_children.send(:add, child) }
283
+ # TODO: create an object that wraps this logic, and when the first
284
+ # kicker is fired, then it'll load up the collection, and then
285
+ # populate all the other methods
120
286
 
121
- if parent_key.get(parent) == parent_value
122
- ret = parents_children
123
- else
124
- association.instance_variable_set(:@children, parents_children)
125
- end
126
- end
287
+ collection = source.model.all(collection_query).each do |target|
288
+ target_maps[target_key.get(target)] << target
289
+ end
127
290
 
128
- ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip([ parent_value ]).to_hash)))
291
+ Array(source).each do |source|
292
+ key = target_key.typecast(source_key.get(source))
293
+ eager_load_targets(source, target_maps[key], query)
129
294
  end
295
+
296
+ collection
130
297
  end
131
298
 
132
- # @api private
133
- def get_parent(child, parent = nil)
134
- child_value = child_key.get(child)
135
- return nil if child_value.any? { |v| v.nil? }
299
+ # Checks if "other end" of association is loaded on given
300
+ # resource.
301
+ #
302
+ # @api semipublic
303
+ def loaded?(resource)
304
+ assert_kind_of 'resource', resource, source_model
136
305
 
137
- with_repository(parent || parent_model) do
138
- parent_identity_map = (parent || parent_model).repository.identity_map(parent_model.base_model)
139
- child_identity_map = child.repository.identity_map(child_model.base_model)
306
+ resource.instance_variable_defined?(instance_variable_name)
307
+ end
140
308
 
141
- if parent = parent_identity_map[child_value]
142
- return parent
143
- end
309
+ # Test the source to see if it is a valid target
310
+ #
311
+ # @param [Object] source
312
+ # the resource or collection to be tested
313
+ #
314
+ # @return [Boolean]
315
+ # true if the resource is valid
316
+ #
317
+ # @api semipulic
318
+ def valid?(source)
319
+ return true if source.nil?
320
+
321
+ case source
322
+ when Array, Collection then valid_collection?(source)
323
+ when Resource then valid_resource?(source)
324
+ else
325
+ raise ArgumentError, "+source+ should be an Array or Resource, but was a #{source.class.name}"
326
+ end
327
+ end
144
328
 
145
- children = child_identity_map.values
146
- children << child unless child_identity_map[child.key]
329
+ # Compares another Relationship for equality
330
+ #
331
+ # @param [Relationship] other
332
+ # the other Relationship to compare with
333
+ #
334
+ # @return [Boolean]
335
+ # true if they are equal, false if not
336
+ #
337
+ # @api public
338
+ def eql?(other)
339
+ return true if equal?(other)
340
+ instance_of?(other.class) && cmp?(other, :eql?)
341
+ end
147
342
 
148
- bind_values = children.map { |c| child_key.get(c) }.uniq
149
- query_values = bind_values.reject { |k| parent_identity_map[k] }
343
+ # Compares another Relationship for equivalency
344
+ #
345
+ # @param [Relationship] other
346
+ # the other Relationship to compare with
347
+ #
348
+ # @return [Boolean]
349
+ # true if they are equal, false if not
350
+ #
351
+ # @api public
352
+ def ==(other)
353
+ return true if equal?(other)
354
+ return false if kind_of_inverse?(other)
355
+ other.respond_to?(:cmp_repository?, true) &&
356
+ other.respond_to?(:cmp_model?, true) &&
357
+ other.respond_to?(:cmp_key?, true) &&
358
+ other.respond_to?(:query) &&
359
+ cmp?(other, :==)
360
+ end
150
361
 
151
- bind_values = query_values unless query_values.empty?
152
- query = parent_key.zip(bind_values.transpose).to_hash
153
- association_accessor = "#{self.name}_association"
362
+ # Get the inverse relationship from the target model
363
+ #
364
+ # @api semipublic
365
+ def inverse
366
+ return @inverse if defined?(@inverse)
154
367
 
155
- collection = parent_model.send(:all, query)
156
- unless collection.empty?
157
- collection.send(:lazy_load)
158
- children.each do |c|
159
- c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
160
- end
161
- child.send(association_accessor).instance_variable_get(:@parent)
162
- end
368
+ if kind_of_inverse?(options[:inverse])
369
+ return @inverse = options[:inverse]
163
370
  end
371
+
372
+ relationships = target_model.relationships(relative_target_repository_name).values
373
+
374
+ @inverse = relationships.detect { |relationship| inverse?(relationship) } ||
375
+ invert
376
+
377
+ @inverse.child_key
378
+
379
+ @inverse
164
380
  end
165
381
 
382
+ # TODO: document
166
383
  # @api private
167
- def with_repository(object = nil)
168
- other_model = object.model == child_model ? parent_model : child_model if object.respond_to?(:model)
169
- other_model = object == child_model ? parent_model : child_model if object.kind_of?(DataMapper::Resource)
384
+ def relative_target_repository_name
385
+ target_repository_name || source_repository_name
386
+ end
170
387
 
171
- if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
172
- object.repository.scope { |block_args| yield(*block_args) }
388
+ # TODO: document
389
+ # @api private
390
+ def relative_target_repository_name_for(source)
391
+ target_repository_name || if source.respond_to?(:repository)
392
+ source.repository.name
173
393
  else
174
- repository(@repository_name) { |block_args| yield(*block_args) }
394
+ source_repository_name
175
395
  end
176
396
  end
177
397
 
398
+ private
399
+
400
+ # TODO: document
178
401
  # @api private
179
- def attach_parent(child, parent)
180
- child_key.set(child, parent && parent_key.get(parent))
181
- end
402
+ attr_reader :child_properties
182
403
 
183
- private
404
+ # TODO: document
405
+ # @api private
406
+ attr_reader :parent_properties
407
+
408
+ # Initializes new Relationship: sets attributes of relationship
409
+ # from options as well as conventions: for instance, @ivar name
410
+ # for association is constructed by prefixing @ to association name.
411
+ #
412
+ # Once attributes are set, reader and writer are created for
413
+ # the resource association belongs to
414
+ #
415
+ # @api semipublic
416
+ def initialize(name, child_model, parent_model, options = {})
417
+ initialize_object_ivar('child_model', child_model)
418
+ initialize_object_ivar('parent_model', parent_model)
419
+
420
+ @name = name
421
+ @instance_variable_name = "@#{@name}".freeze
422
+ @options = options.dup.freeze
423
+ @child_repository_name = @options[:child_repository_name]
424
+ @parent_repository_name = @options[:parent_repository_name]
425
+ @child_properties = @options[:child_key].try_dup.freeze
426
+ @parent_properties = @options[:parent_key].try_dup.freeze
427
+ @min = @options[:min]
428
+ @max = @options[:max]
429
+
430
+ # TODO: normalize the @query to become :conditions => AndOperation
431
+ # - Property/Relationship/Path should be left alone
432
+ # - Symbol/String keys should become a Property, scoped to the target_repository and target_model
433
+ # - Extract subject (target) from Operator
434
+ # - subject should be processed same as above
435
+ # - each subject should be transformed into AbstractComparison
436
+ # object with the subject, operator and value
437
+ # - transform into an AndOperation object, and return the
438
+ # query as :condition => and_object from self.query
439
+ # - this should provide the best performance
440
+
441
+ @query = @options.except(*self.class::OPTIONS).freeze
442
+
443
+ create_reader
444
+ create_writer
445
+ end
184
446
 
185
- # +child_model_name and child_properties refers to the FK, parent_model_name
186
- # and parent_properties refer to the PK. For more information:
187
- # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
188
- # I wash my hands of it!
189
- def initialize(name, repository_name, child_model, parent_model, options = {})
190
- assert_kind_of 'name', name, Symbol
191
- assert_kind_of 'repository_name', repository_name, Symbol
192
- assert_kind_of 'child_model', child_model, String, Class
193
- assert_kind_of 'parent_model', parent_model, String, Class
194
-
195
- unless model_defined?(child_model) || model_defined?(parent_model)
196
- raise 'at least one of child_model and parent_model must be a Model object'
447
+ # Set the correct ivars for the named object
448
+ #
449
+ # This method should set the object in an ivar with the same name
450
+ # provided, plus it should set a String form of the object in
451
+ # a second ivar.
452
+ #
453
+ # @param [String]
454
+ # the name of the ivar to set
455
+ # @param [#name, #to_str, #to_sym] object
456
+ # the object to set in the ivar
457
+ #
458
+ # @return [String]
459
+ # the String value
460
+ #
461
+ # @raise [ArgumentError]
462
+ # raise when object does not respond to expected methods
463
+ #
464
+ # @api private
465
+ def initialize_object_ivar(name, object)
466
+ if object.respond_to?(:name)
467
+ instance_variable_set("@#{name}", object)
468
+ initialize_object_ivar(name, object.name)
469
+ elsif object.respond_to?(:to_str)
470
+ instance_variable_set("@#{name}_name", object.to_str.dup.freeze)
471
+ elsif object.respond_to?(:to_sym)
472
+ instance_variable_set("@#{name}_name", object.to_sym)
473
+ else
474
+ raise ArgumentError, "#{name} does not respond to #to_str or #name"
197
475
  end
198
476
 
199
- if child_properties = options[:child_key]
200
- assert_kind_of 'options[:child_key]', child_properties, Array
477
+ object
478
+ end
479
+
480
+ # Creates reader method for association.
481
+ #
482
+ # Must be implemented by subclasses.
483
+ #
484
+ # @api semipublic
485
+ def create_reader
486
+ raise NotImplementedError, "#{self.class}#create_reader not implemented"
487
+ end
488
+
489
+ # Creates both writer method for association.
490
+ #
491
+ # Must be implemented by subclasses.
492
+ #
493
+ # @api semipublic
494
+ def create_writer
495
+ raise NotImplementedError, "#{self.class}#create_writer not implemented"
496
+ end
497
+
498
+ # Sets the association targets in the resource
499
+ #
500
+ # @param [Resource] source
501
+ # the source to set
502
+ # @param [Array<Resource>] targets
503
+ # the targets for the association
504
+ # @param [Query, Hash] query
505
+ # the query to scope the association with
506
+ #
507
+ # @return [undefined]
508
+ #
509
+ # @api private
510
+ def eager_load_targets(source, targets, query)
511
+ raise NotImplementedError, "#{self.class}#eager_load_targets not implemented"
512
+ end
513
+
514
+ # TODO: document
515
+ # @api private
516
+ def valid_collection?(collection)
517
+ if collection.instance_of?(Array) || collection.loaded?
518
+ collection.all? { |resource| valid_resource?(resource) }
519
+ else
520
+ collection.model <= target_model && (collection.query.fields & target_key) == target_key
201
521
  end
522
+ end
523
+
524
+ # TODO: document
525
+ # @api private
526
+ def valid_resource?(resource)
527
+ resource.kind_of?(target_model) &&
528
+ target_key.zip(target_key.get!(resource)).all? { |property, value| property.valid?(value) }
529
+ end
530
+
531
+ # TODO: document
532
+ # @api private
533
+ def inverse?(other)
534
+ return true if @inverse.equal?(other)
535
+
536
+ other != self &&
537
+ kind_of_inverse?(other) &&
538
+ cmp_repository?(other, :==, :child) &&
539
+ cmp_repository?(other, :==, :parent) &&
540
+ cmp_model?(other, :==, :child) &&
541
+ cmp_model?(other, :==, :parent) &&
542
+ cmp_key?(other, :==, :child) &&
543
+ cmp_key?(other, :==, :parent)
544
+
545
+ # TODO: match only when the Query is empty, or is the same as the
546
+ # default scope for the target model
547
+ end
202
548
 
203
- if parent_properties = options[:parent_key]
204
- assert_kind_of 'options[:parent_key]', parent_properties, Array
549
+ # TODO: document
550
+ # @api private
551
+ def inverse_name
552
+ if options[:inverse].kind_of?(Relationship)
553
+ options[:inverse].name
554
+ else
555
+ options[:inverse]
205
556
  end
557
+ end
558
+
559
+ # TODO: document
560
+ # @api private
561
+ def invert
562
+ inverse_class.new(inverse_name, child_model, parent_model, inverted_options)
563
+ end
564
+
565
+ # TODO: document
566
+ # @api private
567
+ def inverted_options
568
+ options.only(*OPTIONS - [ :min, :max ]).update(:inverse => self)
569
+ end
206
570
 
207
- @name = name
208
- @repository_name = repository_name
209
- @child_model = child_model
210
- @child_properties = child_properties # may be nil
211
- @query = options.reject { |k,v| OPTIONS.include?(k) }
212
- @parent_model = parent_model
213
- @parent_properties = parent_properties # may be nil
214
- @options = options
215
-
216
- # attempt to load the child_key if the parent and child model constants are defined
217
- if model_defined?(@child_model) && model_defined?(@parent_model)
218
- child_key
571
+ # TODO: document
572
+ # @api private
573
+ def options_with_inverse
574
+ if child_model? && parent_model?
575
+ options.merge(:inverse => inverse)
576
+ else
577
+ options.merge(:inverse => inverse_name)
219
578
  end
220
579
  end
221
580
 
581
+ # TODO: document
582
+ # @api private
583
+ def kind_of_inverse?(other)
584
+ other.kind_of?(inverse_class)
585
+ end
586
+
587
+ # TODO: document
222
588
  # @api private
223
- def model_defined?(model)
224
- # TODO: figure out other ways to see if the model is loaded
225
- model.kind_of?(Class)
589
+ def cmp?(other, operator)
590
+ name.send(operator, other.name) &&
591
+ cmp_repository?(other, operator, :child) &&
592
+ cmp_repository?(other, operator, :parent) &&
593
+ cmp_model?(other, operator, :child) &&
594
+ cmp_model?(other, operator, :parent) &&
595
+ cmp_key?(other, operator, :child) &&
596
+ cmp_key?(other, operator, :parent) &&
597
+ query.send(operator, other.query)
598
+ end
599
+
600
+ # TODO: document
601
+ # @api private
602
+ def cmp_repository?(other, operator, type)
603
+ # if either repository is nil, then the relationship is relative,
604
+ # and the repositories are considered equivalent
605
+ return true unless repository_name = send("#{type}_repository_name")
606
+ return true unless other_repository_name = other.send("#{type}_repository_name")
607
+
608
+ repository_name.send(operator, other_repository_name)
609
+ end
610
+
611
+ # TODO: document
612
+ # @api private
613
+ def cmp_model?(other, operator, type)
614
+ send("#{type}_model?") &&
615
+ other.send("#{type}_model?") &&
616
+ send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model)
617
+ end
618
+
619
+ # TODO: document
620
+ # @api private
621
+ def cmp_key?(other, operator, type)
622
+ property_method = "#{type}_properties"
623
+
624
+ self_key = send(property_method)
625
+ other_key = other.send(property_method)
626
+
627
+ self_key.send(operator, other_key)
226
628
  end
227
629
  end # class Relationship
228
630
  end # module Associations