datamapper-dm-core 0.9.11 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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