rpbertp13-dm-core 0.9.11.1

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 (131) hide show
  1. data/.autotest +26 -0
  2. data/.gitignore +18 -0
  3. data/CONTRIBUTING +51 -0
  4. data/FAQ +92 -0
  5. data/History.txt +52 -0
  6. data/MIT-LICENSE +22 -0
  7. data/Manifest.txt +130 -0
  8. data/QUICKLINKS +11 -0
  9. data/README.txt +143 -0
  10. data/Rakefile +32 -0
  11. data/SPECS +62 -0
  12. data/TODO +1 -0
  13. data/dm-core.gemspec +40 -0
  14. data/lib/dm-core.rb +217 -0
  15. data/lib/dm-core/adapters.rb +16 -0
  16. data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
  17. data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
  18. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  19. data/lib/dm-core/adapters/mysql_adapter.rb +138 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  22. data/lib/dm-core/associations.rb +207 -0
  23. data/lib/dm-core/associations/many_to_many.rb +147 -0
  24. data/lib/dm-core/associations/many_to_one.rb +107 -0
  25. data/lib/dm-core/associations/one_to_many.rb +315 -0
  26. data/lib/dm-core/associations/one_to_one.rb +61 -0
  27. data/lib/dm-core/associations/relationship.rb +221 -0
  28. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  29. data/lib/dm-core/auto_migrations.rb +105 -0
  30. data/lib/dm-core/collection.rb +670 -0
  31. data/lib/dm-core/dependency_queue.rb +32 -0
  32. data/lib/dm-core/hook.rb +11 -0
  33. data/lib/dm-core/identity_map.rb +42 -0
  34. data/lib/dm-core/is.rb +16 -0
  35. data/lib/dm-core/logger.rb +232 -0
  36. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  37. data/lib/dm-core/migrator.rb +29 -0
  38. data/lib/dm-core/model.rb +526 -0
  39. data/lib/dm-core/naming_conventions.rb +84 -0
  40. data/lib/dm-core/property.rb +676 -0
  41. data/lib/dm-core/property_set.rb +169 -0
  42. data/lib/dm-core/query.rb +676 -0
  43. data/lib/dm-core/repository.rb +167 -0
  44. data/lib/dm-core/resource.rb +671 -0
  45. data/lib/dm-core/scope.rb +58 -0
  46. data/lib/dm-core/support.rb +7 -0
  47. data/lib/dm-core/support/array.rb +13 -0
  48. data/lib/dm-core/support/assertions.rb +8 -0
  49. data/lib/dm-core/support/errors.rb +23 -0
  50. data/lib/dm-core/support/kernel.rb +11 -0
  51. data/lib/dm-core/support/symbol.rb +41 -0
  52. data/lib/dm-core/transaction.rb +252 -0
  53. data/lib/dm-core/type.rb +160 -0
  54. data/lib/dm-core/type_map.rb +80 -0
  55. data/lib/dm-core/types.rb +19 -0
  56. data/lib/dm-core/types/boolean.rb +7 -0
  57. data/lib/dm-core/types/discriminator.rb +34 -0
  58. data/lib/dm-core/types/object.rb +24 -0
  59. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  60. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  61. data/lib/dm-core/types/serial.rb +9 -0
  62. data/lib/dm-core/types/text.rb +10 -0
  63. data/lib/dm-core/version.rb +3 -0
  64. data/script/all +4 -0
  65. data/script/performance.rb +282 -0
  66. data/script/profile.rb +87 -0
  67. data/spec/integration/association_spec.rb +1382 -0
  68. data/spec/integration/association_through_spec.rb +203 -0
  69. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  70. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  71. data/spec/integration/associations/one_to_many_spec.rb +188 -0
  72. data/spec/integration/auto_migrations_spec.rb +413 -0
  73. data/spec/integration/collection_spec.rb +1073 -0
  74. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  75. data/spec/integration/dependency_queue_spec.rb +46 -0
  76. data/spec/integration/model_spec.rb +197 -0
  77. data/spec/integration/mysql_adapter_spec.rb +85 -0
  78. data/spec/integration/postgres_adapter_spec.rb +731 -0
  79. data/spec/integration/property_spec.rb +253 -0
  80. data/spec/integration/query_spec.rb +514 -0
  81. data/spec/integration/repository_spec.rb +61 -0
  82. data/spec/integration/resource_spec.rb +513 -0
  83. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  84. data/spec/integration/sti_spec.rb +273 -0
  85. data/spec/integration/strategic_eager_loading_spec.rb +156 -0
  86. data/spec/integration/transaction_spec.rb +60 -0
  87. data/spec/integration/type_spec.rb +275 -0
  88. data/spec/lib/logging_helper.rb +18 -0
  89. data/spec/lib/mock_adapter.rb +27 -0
  90. data/spec/lib/model_loader.rb +100 -0
  91. data/spec/lib/publicize_methods.rb +28 -0
  92. data/spec/models/content.rb +16 -0
  93. data/spec/models/vehicles.rb +34 -0
  94. data/spec/models/zoo.rb +48 -0
  95. data/spec/spec.opts +3 -0
  96. data/spec/spec_helper.rb +91 -0
  97. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  98. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  99. data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
  100. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  101. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  102. data/spec/unit/associations/many_to_many_spec.rb +32 -0
  103. data/spec/unit/associations/many_to_one_spec.rb +159 -0
  104. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  105. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  106. data/spec/unit/associations/relationship_spec.rb +71 -0
  107. data/spec/unit/associations_spec.rb +242 -0
  108. data/spec/unit/auto_migrations_spec.rb +111 -0
  109. data/spec/unit/collection_spec.rb +182 -0
  110. data/spec/unit/data_mapper_spec.rb +35 -0
  111. data/spec/unit/identity_map_spec.rb +126 -0
  112. data/spec/unit/is_spec.rb +80 -0
  113. data/spec/unit/migrator_spec.rb +33 -0
  114. data/spec/unit/model_spec.rb +321 -0
  115. data/spec/unit/naming_conventions_spec.rb +36 -0
  116. data/spec/unit/property_set_spec.rb +90 -0
  117. data/spec/unit/property_spec.rb +753 -0
  118. data/spec/unit/query_spec.rb +571 -0
  119. data/spec/unit/repository_spec.rb +93 -0
  120. data/spec/unit/resource_spec.rb +649 -0
  121. data/spec/unit/scope_spec.rb +142 -0
  122. data/spec/unit/transaction_spec.rb +469 -0
  123. data/spec/unit/type_map_spec.rb +114 -0
  124. data/spec/unit/type_spec.rb +119 -0
  125. data/tasks/ci.rb +36 -0
  126. data/tasks/dm.rb +63 -0
  127. data/tasks/doc.rb +20 -0
  128. data/tasks/gemspec.rb +23 -0
  129. data/tasks/hoe.rb +46 -0
  130. data/tasks/install.rb +20 -0
  131. metadata +215 -0
@@ -0,0 +1,315 @@
1
+ module DataMapper
2
+ module Associations
3
+ module OneToMany
4
+ extend Assertions
5
+
6
+ # Setup one to many relationship between two models
7
+ # -
8
+ # @api private
9
+ def self.setup(name, model, options = {})
10
+ assert_kind_of 'name', name, Symbol
11
+ assert_kind_of 'model', model, Model
12
+ assert_kind_of 'options', options, Hash
13
+
14
+ repository_name = model.repository.name
15
+
16
+ model.class_eval <<-EOS, __FILE__, __LINE__
17
+ def #{name}(query = {})
18
+ #{name}_association.all(query)
19
+ end
20
+
21
+ def #{name}=(children)
22
+ #{name}_association.replace(children)
23
+ end
24
+
25
+ private
26
+
27
+ def #{name}_association
28
+ @#{name}_association ||= begin
29
+ unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
+ end
32
+ association = Proxy.new(relationship, self)
33
+ parent_associations << association
34
+ association
35
+ end
36
+ end
37
+ EOS
38
+
39
+ model.relationships(repository_name)[name] = if options.has_key?(:through)
40
+ opts = options.dup
41
+
42
+ if opts.key?(:class_name) && !opts.key?(:child_key)
43
+ warn(<<-EOS.margin)
44
+ You have specified #{model.base_model.name}.has(#{name.inspect}) with :class_name => #{opts[:class_name].inspect}. You probably also want to specify the :child_key option.
45
+ EOS
46
+ end
47
+
48
+ opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
49
+ opts[:parent_model] = model
50
+ opts[:repository_name] = repository_name
51
+ opts[:near_relationship_name] = opts.delete(:through)
52
+ opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
53
+ opts[:parent_key] = opts[:parent_key]
54
+ opts[:child_key] = opts[:child_key]
55
+
56
+ RelationshipChain.new( opts )
57
+ else
58
+ Relationship.new(
59
+ name,
60
+ repository_name,
61
+ options.fetch(:class_name, Extlib::Inflection.classify(name)),
62
+ model,
63
+ options
64
+ )
65
+ end
66
+ end
67
+
68
+ # TODO: look at making this inherit from Collection. The API is
69
+ # almost identical, and it would make more sense for the
70
+ # relationship.get_children method to return a Proxy than a
71
+ # Collection that is wrapped in a Proxy.
72
+ class Proxy
73
+ include Assertions
74
+
75
+ instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class object_id kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m.to_s) }
76
+
77
+ # FIXME: remove when RelationshipChain#get_children can return a Collection
78
+ def all(query = {})
79
+ query.empty? ? self : @relationship.get_children(@parent, query)
80
+ end
81
+
82
+ # FIXME: remove when RelationshipChain#get_children can return a Collection
83
+ def first(*args)
84
+ if args.last.respond_to?(:merge)
85
+ query = args.pop
86
+ @relationship.get_children(@parent, query, :first, *args)
87
+ else
88
+ children.first(*args)
89
+ end
90
+ end
91
+
92
+ def <<(resource)
93
+ assert_mutable
94
+ return self if !resource.new_record? && self.include?(resource)
95
+ children << resource
96
+ relate_resource(resource)
97
+ self
98
+ end
99
+
100
+ def push(*resources)
101
+ assert_mutable
102
+ resources.reject! { |resource| !resource.new_record? && self.include?(resource) }
103
+ children.push(*resources)
104
+ resources.each { |resource| relate_resource(resource) }
105
+ self
106
+ end
107
+
108
+ def unshift(*resources)
109
+ assert_mutable
110
+ resources.reject! { |resource| !resource.new_record? && self.include?(resource) }
111
+ children.unshift(*resources)
112
+ resources.each { |resource| relate_resource(resource) }
113
+ self
114
+ end
115
+
116
+ def replace(other)
117
+ assert_mutable
118
+ each { |resource| orphan_resource(resource) }
119
+ other = other.map { |resource| resource.kind_of?(Hash) ? new_child(resource) : resource }
120
+ children.replace(other)
121
+ other.each { |resource| relate_resource(resource) }
122
+ self
123
+ end
124
+
125
+ def pop
126
+ assert_mutable
127
+ orphan_resource(children.pop)
128
+ end
129
+
130
+ def shift
131
+ assert_mutable
132
+ orphan_resource(children.shift)
133
+ end
134
+
135
+ def delete(resource)
136
+ assert_mutable
137
+ orphan_resource(children.delete(resource))
138
+ end
139
+
140
+ def delete_at(index)
141
+ assert_mutable
142
+ orphan_resource(children.delete_at(index))
143
+ end
144
+
145
+ def clear
146
+ assert_mutable
147
+ each { |resource| orphan_resource(resource) }
148
+ children.clear
149
+ self
150
+ end
151
+
152
+ def build(attributes = {})
153
+ assert_mutable
154
+ attributes = default_attributes.merge(attributes)
155
+ resource = children.respond_to?(:build) ? children.build(attributes) : new_child(attributes)
156
+ resource
157
+ end
158
+
159
+ def new(attributes = {})
160
+ assert_mutable
161
+ raise UnsavedParentError, 'You cannot intialize until the parent is saved' if @parent.new_record?
162
+ attributes = default_attributes.merge(attributes)
163
+ resource = children.respond_to?(:new) ? children.new(attributes) : @relationship.child_model.new(attributes)
164
+ self << resource
165
+ resource
166
+ end
167
+
168
+ def create(attributes = {})
169
+ assert_mutable
170
+ raise UnsavedParentError, 'You cannot create until the parent is saved' if @parent.new_record?
171
+ attributes = default_attributes.merge(attributes)
172
+ resource = children.respond_to?(:create) ? children.create(attributes) : @relationship.child_model.create(attributes)
173
+ self << resource
174
+ resource
175
+ end
176
+
177
+ def update(attributes = {})
178
+ assert_mutable
179
+ raise UnsavedParentError, 'You cannot mass-update until the parent is saved' if @parent.new_record?
180
+ children.update(attributes)
181
+ end
182
+
183
+ def update!(attributes = {})
184
+ assert_mutable
185
+ raise UnsavedParentError, 'You cannot mass-update without validations until the parent is saved' if @parent.new_record?
186
+ children.update!(attributes)
187
+ end
188
+
189
+ def destroy
190
+ assert_mutable
191
+ raise UnsavedParentError, 'You cannot mass-delete until the parent is saved' if @parent.new_record?
192
+ children.destroy
193
+ end
194
+
195
+ def destroy!
196
+ assert_mutable
197
+ raise UnsavedParentError, 'You cannot mass-delete without validations until the parent is saved' if @parent.new_record?
198
+ children.destroy!
199
+ end
200
+
201
+ def reload
202
+ @children = nil
203
+ self
204
+ end
205
+
206
+ def save
207
+ return true if children.frozen?
208
+
209
+ # save every resource in the collection
210
+ each { |resource| save_resource(resource) }
211
+
212
+ # save orphan resources
213
+ @orphans.each do |resource|
214
+ begin
215
+ save_resource(resource, nil)
216
+ rescue
217
+ children << resource unless children.frozen? || children.include?(resource)
218
+ raise
219
+ end
220
+ end
221
+
222
+ # FIXME: remove when RelationshipChain#get_children can return a Collection
223
+ # place the children into a Collection if not already
224
+ if children.kind_of?(Array) && !children.frozen?
225
+ @children = @relationship.get_children(@parent).replace(children)
226
+ end
227
+
228
+ true
229
+ end
230
+
231
+ def kind_of?(klass)
232
+ super || children.kind_of?(klass)
233
+ end
234
+
235
+ def respond_to?(method, include_private = false)
236
+ super || children.respond_to?(method, include_private)
237
+ end
238
+
239
+ private
240
+
241
+ def initialize(relationship, parent)
242
+ assert_kind_of 'relationship', relationship, Relationship
243
+ assert_kind_of 'parent', parent, Resource
244
+
245
+ @relationship = relationship
246
+ @parent = parent
247
+ @orphans = []
248
+ end
249
+
250
+ def children
251
+ @children ||= @relationship.get_children(@parent)
252
+ end
253
+
254
+ def assert_mutable
255
+ raise ImmutableAssociationError, 'You can not modify this association' if children.frozen?
256
+ end
257
+
258
+ def default_attributes
259
+ default_attributes = {}
260
+
261
+ @relationship.query.each do |attribute, value|
262
+ next if Query::OPTIONS.include?(attribute) || attribute.kind_of?(Query::Operator)
263
+ default_attributes[attribute] = value
264
+ end
265
+
266
+ @relationship.child_key.zip(@relationship.parent_key.get(@parent)) do |property,value|
267
+ default_attributes[property.name] = value
268
+ end
269
+
270
+ default_attributes
271
+ end
272
+
273
+ def add_default_association_values(resource)
274
+ default_attributes.each do |attribute, value|
275
+ next if !resource.respond_to?("#{attribute}=") || resource.attribute_loaded?(attribute)
276
+ resource.send("#{attribute}=", value)
277
+ end
278
+ end
279
+
280
+ def new_child(attributes)
281
+ @relationship.child_model.new(default_attributes.merge(attributes))
282
+ end
283
+
284
+ def relate_resource(resource)
285
+ assert_mutable
286
+ add_default_association_values(resource)
287
+ @orphans.delete(resource)
288
+ resource
289
+ end
290
+
291
+ def orphan_resource(resource)
292
+ assert_mutable
293
+ @orphans << resource
294
+ resource
295
+ end
296
+
297
+ def save_resource(resource, parent = @parent)
298
+ @relationship.with_repository(resource) do |r|
299
+ if parent.nil? && resource.model.respond_to?(:many_to_many)
300
+ resource.destroy
301
+ else
302
+ @relationship.attach_parent(resource, parent)
303
+ resource.save
304
+ end
305
+ end
306
+ end
307
+
308
+ def method_missing(method, *args, &block)
309
+ results = children.send(method, *args, &block)
310
+ results.equal?(children) ? self : results
311
+ end
312
+ end # class Proxy
313
+ end # module OneToMany
314
+ end # module Associations
315
+ end # module DataMapper
@@ -0,0 +1,61 @@
1
+ module DataMapper
2
+ module Associations
3
+ module OneToOne
4
+ extend Assertions
5
+
6
+ # Setup one to one relationship between two models
7
+ # -
8
+ # @api private
9
+ def self.setup(name, model, options = {})
10
+ assert_kind_of 'name', name, Symbol
11
+ assert_kind_of 'model', model, Model
12
+ assert_kind_of 'options', options, Hash
13
+
14
+ repository_name = model.repository.name
15
+
16
+ model.class_eval <<-EOS, __FILE__, __LINE__
17
+ def #{name}
18
+ #{name}_association.first
19
+ end
20
+
21
+ def #{name}=(child_resource)
22
+ #{name}_association.replace(child_resource.nil? ? [] : [ child_resource ])
23
+ end
24
+
25
+ private
26
+
27
+ def #{name}_association
28
+ @#{name}_association ||= begin
29
+ unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
+ end
32
+ association = Associations::OneToMany::Proxy.new(relationship, self)
33
+ parent_associations << association
34
+ association
35
+ end
36
+ end
37
+ EOS
38
+
39
+ model.relationships(repository_name)[name] = if options.has_key?(:through)
40
+ RelationshipChain.new(
41
+ :child_model => options.fetch(:class_name, Extlib::Inflection.classify(name)),
42
+ :parent_model => model,
43
+ :repository_name => repository_name,
44
+ :near_relationship_name => options[:through],
45
+ :remote_relationship_name => options.fetch(:remote_name, name),
46
+ :parent_key => options[:parent_key],
47
+ :child_key => options[:child_key]
48
+ )
49
+ else
50
+ Relationship.new(
51
+ name,
52
+ repository_name,
53
+ options.fetch(:class_name, Extlib::Inflection.classify(name)),
54
+ model,
55
+ options
56
+ )
57
+ end
58
+ end
59
+ end # module HasOne
60
+ end # module Associations
61
+ end # module DataMapper
@@ -0,0 +1,221 @@
1
+ module DataMapper
2
+ module Associations
3
+ class Relationship
4
+ include Assertions
5
+
6
+ OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :through ]
7
+
8
+ # @api private
9
+ attr_reader :name, :options, :query
10
+
11
+ def child_key
12
+ child_key = child_model.repository.scope do |r|
13
+ model_properties = child_model.properties(r.name)
14
+
15
+ child_key = parent_key.zip(@child_properties).map do |parent_property,property_name|
16
+ # TODO: use something similar to DM::NamingConventions to determine the property name
17
+ parent_name = Extlib::Inflection.underscore(Extlib::Inflection.demodulize(parent_model.base_model.name))
18
+ property_name ||= "#{parent_name}_#{parent_property.name}".to_sym
19
+
20
+ if model_properties.has_property?(property_name)
21
+ model_properties[property_name]
22
+ else
23
+ options = {}
24
+
25
+ [ :length, :precision, :scale ].each do |option|
26
+ options[option] = parent_property.send(option)
27
+ end
28
+
29
+ # NOTE: hack to make each many to many child_key a true key,
30
+ # until I can figure out a better place for this check
31
+ if child_model.respond_to?(:many_to_many)
32
+ options[:key] = true
33
+ end
34
+
35
+ child_model.property(property_name, parent_property.primitive, options)
36
+ end
37
+ end
38
+ end
39
+ @child_key = PropertySet.new(child_key)
40
+ end
41
+
42
+ # @api private
43
+ def parent_key
44
+ parent_key = parent_model.repository.scope do |r|
45
+ if @parent_properties
46
+ parent_model.properties(r.name).slice(*@parent_properties)
47
+ else
48
+ parent_model.key
49
+ end
50
+ end
51
+ @parent_key = PropertySet.new(parent_key)
52
+ end
53
+
54
+ # @api private
55
+ def parent_model
56
+ return @parent_model if model_defined?(@parent_model)
57
+ @parent_model = @child_model.find_const(@parent_model)
58
+ rescue NameError
59
+ raise NameError, "Cannot find the parent_model #{@parent_model} for #{@child_model}"
60
+ end
61
+
62
+ # @api private
63
+ def child_model
64
+ return @child_model if model_defined?(@child_model)
65
+ @child_model = @parent_model.find_const(@child_model)
66
+ rescue NameError
67
+ raise NameError, "Cannot find the child_model #{@child_model} for #{@parent_model}"
68
+ end
69
+
70
+ # @api private
71
+ def get_children(parent, options = {}, finder = :all, *args)
72
+ parent_value = parent_key.get(parent)
73
+ bind_values = [ parent_value ]
74
+
75
+ with_repository(child_model) do |r|
76
+ parent_identity_map = parent.repository.identity_map(parent_model)
77
+
78
+ query_values = parent_identity_map.keys
79
+
80
+ bind_values = query_values unless query_values.empty?
81
+ query = child_key.zip(bind_values.transpose).to_hash
82
+
83
+ collection = child_model.send(finder, *(args.dup << @query.merge(options).merge(query)))
84
+
85
+ return collection unless collection.kind_of?(Collection) && collection.any?
86
+
87
+ grouped_collection = {}
88
+ collection.each do |resource|
89
+ child_value = child_key.get(resource)
90
+ next unless parent_obj = parent_identity_map[child_value]
91
+ grouped_collection[parent_obj] ||= []
92
+ grouped_collection[parent_obj] << resource
93
+ end
94
+
95
+ association_accessor = "#{self.name}_association"
96
+
97
+ ret = nil
98
+ grouped_collection.each do |parent, children|
99
+ association = parent.send(association_accessor)
100
+
101
+ query = collection.query.dup
102
+ query.conditions.map! do |operator, property, bind_value|
103
+ property = property.property if property.class == DataMapper::Query::Path
104
+ if operator != :raw && child_key.has_property?(property.name)
105
+ bind_value = *children.map { |child| property.get(child) }.uniq
106
+ end
107
+ [ operator, property, bind_value ]
108
+ end
109
+
110
+ parents_children = Collection.new(query)
111
+ children.each { |child| parents_children.send(:add, child) }
112
+
113
+ if parent_key.get(parent) == parent_value
114
+ ret = parents_children
115
+ else
116
+ association.instance_variable_set(:@children, parents_children)
117
+ end
118
+ end
119
+
120
+ ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip([ parent_value ]).to_hash)))
121
+ end
122
+ end
123
+
124
+ # @api private
125
+ def get_parent(child, parent = nil)
126
+ child_value = child_key.get(child)
127
+ return nil if child_value.any? { |v| v.nil? }
128
+
129
+ with_repository(parent || parent_model) do
130
+ parent_identity_map = (parent || parent_model).repository.identity_map(parent_model.base_model)
131
+ child_identity_map = child.repository.identity_map(child_model.base_model)
132
+
133
+ if parent = parent_identity_map[child_value]
134
+ return parent
135
+ end
136
+
137
+ children = child_identity_map.values
138
+ children << child unless child_identity_map[child.key]
139
+
140
+ bind_values = children.map { |c| child_key.get(c) }.uniq
141
+ query_values = bind_values.reject { |k| parent_identity_map[k] }
142
+
143
+ bind_values = query_values unless query_values.empty?
144
+ query = parent_key.zip(bind_values.transpose).to_hash
145
+ association_accessor = "#{self.name}_association"
146
+
147
+ collection = parent_model.send(:all, query)
148
+ unless collection.empty?
149
+ collection.send(:lazy_load)
150
+ children.each do |c|
151
+ c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
152
+ end
153
+ child.send(association_accessor).instance_variable_get(:@parent)
154
+ end
155
+ end
156
+ end
157
+
158
+ # @api private
159
+ def with_repository(object = nil)
160
+ other_model = object.model == child_model ? parent_model : child_model if object.respond_to?(:model)
161
+ other_model = object == child_model ? parent_model : child_model if object.kind_of?(DataMapper::Resource)
162
+
163
+ if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
164
+ object.repository.scope { |block_args| yield(*block_args) }
165
+ else
166
+ repository(@repository_name) { |block_args| yield(*block_args) }
167
+ end
168
+ end
169
+
170
+ # @api private
171
+ def attach_parent(child, parent)
172
+ child_key.set(child, parent && parent_key.get(parent))
173
+ end
174
+
175
+ private
176
+
177
+ # +child_model_name and child_properties refers to the FK, parent_model_name
178
+ # and parent_properties refer to the PK. For more information:
179
+ # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
180
+ # I wash my hands of it!
181
+ def initialize(name, repository_name, child_model, parent_model, options = {})
182
+ assert_kind_of 'name', name, Symbol
183
+ assert_kind_of 'repository_name', repository_name, Symbol
184
+ assert_kind_of 'child_model', child_model, String, Class
185
+ assert_kind_of 'parent_model', parent_model, String, Class
186
+
187
+ unless model_defined?(child_model) || model_defined?(parent_model)
188
+ raise 'at least one of child_model and parent_model must be a Model object'
189
+ end
190
+
191
+ if child_properties = options[:child_key]
192
+ assert_kind_of 'options[:child_key]', child_properties, Array
193
+ end
194
+
195
+ if parent_properties = options[:parent_key]
196
+ assert_kind_of 'options[:parent_key]', parent_properties, Array
197
+ end
198
+
199
+ @name = name
200
+ @repository_name = repository_name
201
+ @child_model = child_model
202
+ @child_properties = child_properties # may be nil
203
+ @query = options.reject { |k,v| OPTIONS.include?(k) }
204
+ @parent_model = parent_model
205
+ @parent_properties = parent_properties # may be nil
206
+ @options = options
207
+
208
+ # attempt to load the child_key if the parent and child model constants are defined
209
+ if model_defined?(@child_model) && model_defined?(@parent_model)
210
+ child_key
211
+ end
212
+ end
213
+
214
+ # @api private
215
+ def model_defined?(model)
216
+ # TODO: figure out other ways to see if the model is loaded
217
+ model.kind_of?(Class)
218
+ end
219
+ end # class Relationship
220
+ end # module Associations
221
+ end # module DataMapper