datamapper-dm-core 0.9.11

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 +41 -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 +30 -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 +136 -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 +229 -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 +267 -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 +75 -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 +493 -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,229 @@
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
+ # @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
44
+ end
45
+
46
+ # @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)
58
+ end
59
+ end
60
+
61
+ # @api private
62
+ def parent_model
63
+ return @parent_model if model_defined?(@parent_model)
64
+ @parent_model = @child_model.find_const(@parent_model)
65
+ rescue NameError
66
+ raise NameError, "Cannot find the parent_model #{@parent_model} for #{@child_model}"
67
+ end
68
+
69
+ # @api private
70
+ def child_model
71
+ return @child_model if model_defined?(@child_model)
72
+ @child_model = @parent_model.find_const(@child_model)
73
+ rescue NameError
74
+ raise NameError, "Cannot find the child_model #{@child_model} for #{@parent_model}"
75
+ end
76
+
77
+ # @api private
78
+ def get_children(parent, options = {}, finder = :all, *args)
79
+ parent_value = parent_key.get(parent)
80
+ bind_values = [ parent_value ]
81
+
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)
85
+
86
+ query_values = parent_identity_map.keys
87
+ query_values.reject! { |k| child_identity_map[k] }
88
+
89
+ bind_values = query_values unless query_values.empty?
90
+ query = child_key.zip(bind_values.transpose).to_hash
91
+
92
+ collection = child_model.send(finder, *(args.dup << @query.merge(options).merge(query)))
93
+
94
+ return collection unless collection.kind_of?(Collection) && collection.any?
95
+
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
103
+
104
+ association_accessor = "#{self.name}_association"
105
+
106
+ ret = nil
107
+ grouped_collection.each do |parent, children|
108
+ association = parent.send(association_accessor)
109
+
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
117
+
118
+ parents_children = Collection.new(query)
119
+ children.each { |child| parents_children.send(:add, child) }
120
+
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
127
+
128
+ ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip([ parent_value ]).to_hash)))
129
+ end
130
+ end
131
+
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? }
136
+
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)
140
+
141
+ if parent = parent_identity_map[child_value]
142
+ return parent
143
+ end
144
+
145
+ children = child_identity_map.values
146
+ children << child unless child_identity_map[child.key]
147
+
148
+ bind_values = children.map { |c| child_key.get(c) }.uniq
149
+ query_values = bind_values.reject { |k| parent_identity_map[k] }
150
+
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"
154
+
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
163
+ end
164
+ end
165
+
166
+ # @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)
170
+
171
+ if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
172
+ object.repository.scope { |block_args| yield(*block_args) }
173
+ else
174
+ repository(@repository_name) { |block_args| yield(*block_args) }
175
+ end
176
+ end
177
+
178
+ # @api private
179
+ def attach_parent(child, parent)
180
+ child_key.set(child, parent && parent_key.get(parent))
181
+ end
182
+
183
+ private
184
+
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'
197
+ end
198
+
199
+ if child_properties = options[:child_key]
200
+ assert_kind_of 'options[:child_key]', child_properties, Array
201
+ end
202
+
203
+ if parent_properties = options[:parent_key]
204
+ assert_kind_of 'options[:parent_key]', parent_properties, Array
205
+ end
206
+
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
219
+ end
220
+ end
221
+
222
+ # @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)
226
+ end
227
+ end # class Relationship
228
+ end # module Associations
229
+ end # module DataMapper