dm-core 0.9.2 → 0.9.3

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 (84) hide show
  1. data/.autotest +26 -0
  2. data/{CHANGELOG → History.txt} +78 -77
  3. data/Manifest.txt +123 -0
  4. data/{README → README.txt} +0 -0
  5. data/Rakefile +29 -0
  6. data/SPECS +63 -0
  7. data/TODO +1 -0
  8. data/lib/dm-core.rb +6 -1
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +29 -32
  10. data/lib/dm-core/adapters/mysql_adapter.rb +1 -1
  11. data/lib/dm-core/adapters/postgres_adapter.rb +1 -1
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +2 -2
  13. data/lib/dm-core/associations.rb +26 -0
  14. data/lib/dm-core/associations/many_to_many.rb +34 -25
  15. data/lib/dm-core/associations/many_to_one.rb +4 -4
  16. data/lib/dm-core/associations/one_to_many.rb +48 -13
  17. data/lib/dm-core/associations/one_to_one.rb +4 -4
  18. data/lib/dm-core/associations/relationship.rb +144 -42
  19. data/lib/dm-core/associations/relationship_chain.rb +31 -24
  20. data/lib/dm-core/auto_migrations.rb +0 -4
  21. data/lib/dm-core/collection.rb +40 -7
  22. data/lib/dm-core/dependency_queue.rb +31 -0
  23. data/lib/dm-core/hook.rb +2 -2
  24. data/lib/dm-core/is.rb +2 -2
  25. data/lib/dm-core/logger.rb +10 -10
  26. data/lib/dm-core/model.rb +94 -41
  27. data/lib/dm-core/property.rb +72 -41
  28. data/lib/dm-core/property_set.rb +8 -14
  29. data/lib/dm-core/query.rb +34 -9
  30. data/lib/dm-core/repository.rb +0 -0
  31. data/lib/dm-core/resource.rb +13 -13
  32. data/lib/dm-core/scope.rb +25 -2
  33. data/lib/dm-core/type.rb +3 -3
  34. data/lib/dm-core/types/discriminator.rb +10 -8
  35. data/lib/dm-core/types/object.rb +4 -0
  36. data/lib/dm-core/types/paranoid_boolean.rb +15 -4
  37. data/lib/dm-core/types/paranoid_datetime.rb +15 -4
  38. data/lib/dm-core/version.rb +3 -0
  39. data/script/all +5 -0
  40. data/script/performance.rb +191 -0
  41. data/script/profile.rb +86 -0
  42. data/spec/integration/association_spec.rb +288 -204
  43. data/spec/integration/association_through_spec.rb +9 -3
  44. data/spec/integration/associations/many_to_many_spec.rb +97 -31
  45. data/spec/integration/associations/many_to_one_spec.rb +41 -6
  46. data/spec/integration/associations/one_to_many_spec.rb +18 -2
  47. data/spec/integration/auto_migrations_spec.rb +0 -0
  48. data/spec/integration/collection_spec.rb +89 -42
  49. data/spec/integration/dependency_queue_spec.rb +58 -0
  50. data/spec/integration/model_spec.rb +67 -8
  51. data/spec/integration/postgres_adapter_spec.rb +19 -20
  52. data/spec/integration/property_spec.rb +17 -8
  53. data/spec/integration/query_spec.rb +273 -191
  54. data/spec/integration/resource_spec.rb +108 -10
  55. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  56. data/spec/integration/transaction_spec.rb +3 -3
  57. data/spec/integration/type_spec.rb +121 -0
  58. data/spec/lib/logging_helper.rb +18 -0
  59. data/spec/lib/model_loader.rb +91 -0
  60. data/spec/lib/publicize_methods.rb +28 -0
  61. data/spec/models/vehicles.rb +34 -0
  62. data/spec/models/zoo.rb +48 -0
  63. data/spec/spec.opts +3 -0
  64. data/spec/spec_helper.rb +25 -62
  65. data/spec/unit/adapters/data_objects_adapter_spec.rb +1 -0
  66. data/spec/unit/associations/many_to_many_spec.rb +3 -0
  67. data/spec/unit/associations/many_to_one_spec.rb +9 -1
  68. data/spec/unit/associations/one_to_many_spec.rb +12 -4
  69. data/spec/unit/associations/relationship_spec.rb +19 -15
  70. data/spec/unit/associations_spec.rb +37 -0
  71. data/spec/unit/collection_spec.rb +8 -0
  72. data/spec/unit/data_mapper_spec.rb +14 -0
  73. data/spec/unit/model_spec.rb +2 -2
  74. data/spec/unit/property_set_spec.rb +0 -13
  75. data/spec/unit/property_spec.rb +92 -21
  76. data/spec/unit/query_spec.rb +49 -4
  77. data/spec/unit/resource_spec.rb +122 -60
  78. data/spec/unit/scope_spec.rb +11 -0
  79. data/tasks/ci.rb +68 -0
  80. data/tasks/dm.rb +63 -0
  81. data/tasks/doc.rb +20 -0
  82. data/tasks/hoe.rb +38 -0
  83. data/tasks/install.rb +20 -0
  84. metadata +63 -22
@@ -1,4 +1,4 @@
1
- gem 'do_mysql', '=0.9.2'
1
+ gem 'do_mysql', '=0.9.3'
2
2
  require 'do_mysql'
3
3
 
4
4
  module DataMapper
@@ -1,4 +1,4 @@
1
- gem 'do_postgres', '=0.9.2'
1
+ gem 'do_postgres', '=0.9.3'
2
2
  require 'do_postgres'
3
3
 
4
4
  module DataMapper
@@ -1,4 +1,4 @@
1
- gem 'do_sqlite3', '=0.9.2'
1
+ gem 'do_sqlite3', '=0.9.3'
2
2
  require 'do_sqlite3'
3
3
 
4
4
  module DataMapper
@@ -41,7 +41,7 @@ module DataMapper
41
41
  end
42
42
 
43
43
  module SQL
44
- private
44
+ # private ## This cannot be private for current migrations
45
45
 
46
46
  # TODO: move to dm-more/dm-migrations
47
47
  def supports_serial?
@@ -17,6 +17,32 @@ module DataMapper
17
17
  class UnsavedParentError < RuntimeError
18
18
  end
19
19
 
20
+ # Returns all relationships that are many-to-one for this model.
21
+ #
22
+ # Used to find the relationships that require properties in any Repository.
23
+ #
24
+ # Example:
25
+ # class Plur
26
+ # include DataMapper::Resource
27
+ # def self.default_repository_name
28
+ # :plur_db
29
+ # end
30
+ # repository(:plupp_db) do
31
+ # has 1, :plupp
32
+ # end
33
+ # end
34
+ #
35
+ # This resource has a many-to-one to the Plupp resource residing in the :plupp_db repository,
36
+ # but the Plur resource needs the plupp_id property no matter what repository itself lives in,
37
+ # ie we need to create that property when we migrate etc.
38
+ #
39
+ # Used in DataMapper::Model.properties_with_subclasses
40
+ #
41
+ # @api private
42
+ def many_to_one_relationships
43
+ @relationships.values.collect do |rels| rels.values end.flatten.select do |relationship| relationship.child_model == self end
44
+ end
45
+
20
46
  def relationships(repository_name = default_repository_name)
21
47
  @relationships ||= Hash.new { |h,k| h[k] = k == Repository.default_name ? {} : h[Repository.default_name].dup }
22
48
  @relationships[repository_name]
@@ -28,7 +28,7 @@ module DataMapper
28
28
  def #{name}_association
29
29
  @#{name}_association ||= begin
30
30
  unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
31
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
31
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
32
32
  end
33
33
  association = Proxy.new(relationship, self)
34
34
  parent_associations << association
@@ -40,14 +40,14 @@ module DataMapper
40
40
  opts = options.dup
41
41
  opts.delete(:through)
42
42
  opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
43
- opts[:parent_model] = model.name
43
+ opts[:parent_model] = model
44
44
  opts[:repository_name] = repository_name
45
45
  opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
46
46
  opts[:parent_key] = opts[:parent_key]
47
47
  opts[:child_key] = opts[:child_key]
48
48
  opts[:mutable] = true
49
49
 
50
- names = [ opts[:child_model], opts[:parent_model] ].sort
50
+ names = [ opts[:child_model], opts[:parent_model].name ].sort
51
51
  model_name = names.join
52
52
  storage_name = Extlib::Inflection.tableize(Extlib::Inflection.pluralize(names[0]) + names[1])
53
53
 
@@ -77,39 +77,50 @@ module DataMapper
77
77
  end
78
78
 
79
79
  class Proxy < DataMapper::Associations::OneToMany::Proxy
80
-
81
- def <<(resource)
82
- resource.save if resource.new_record?
83
- through = @relationship.child_model.new
84
- @relationship.child_key.each_with_index do |key, index|
85
- through.send("#{key.name}=", @relationship.parent_key.key[index].get(@parent))
86
- end
87
- remote_relationship.child_key.each_with_index do |key, index|
88
- through.send("#{key.name}=", remote_relationship.parent_key.key[index].get(resource))
89
- end
90
- near_model << through
91
- super
92
- end
93
-
94
80
  def delete(resource)
95
- through = near_model.get(*(@parent.key + resource.key))
96
- near_model.delete(through)
81
+ through = near_association.get(*(@parent.key + resource.key))
82
+ near_association.delete(through)
97
83
  orphan_resource(super)
98
84
  end
99
85
 
100
86
  def clear
101
- near_model.clear
87
+ near_association.clear
102
88
  super
103
89
  end
104
90
 
105
91
  def destroy
106
- near_model.destroy
92
+ near_association.destroy
107
93
  super
108
94
  end
109
95
 
110
96
  def save
111
97
  end
112
98
 
99
+ private
100
+
101
+ def new_child(attributes)
102
+ remote_relationship.parent_model.new(attributes)
103
+ end
104
+
105
+ def relate_resource(resource)
106
+ assert_mutable
107
+ add_default_association_values(resource)
108
+ @orphans.delete(resource)
109
+
110
+ # TODO: fix this so it does not automatically save on append, if possible
111
+ resource.save if resource.new_record?
112
+ through_resource = @relationship.child_model.new
113
+ @relationship.child_key.zip(@relationship.parent_key) do |child_key,parent_key|
114
+ through_resource.send("#{child_key.name}=", parent_key.get(@parent))
115
+ end
116
+ remote_relationship.child_key.zip(remote_relationship.parent_key) do |child_key,parent_key|
117
+ through_resource.send("#{child_key.name}=", parent_key.get(resource))
118
+ end
119
+ near_association << through_resource
120
+
121
+ resource
122
+ end
123
+
113
124
  def orphan_resource(resource)
114
125
  assert_mutable
115
126
  @orphans << resource
@@ -119,14 +130,12 @@ module DataMapper
119
130
  def assert_mutable
120
131
  end
121
132
 
122
- private
123
-
124
133
  def remote_relationship
125
134
  @remote_relationship ||= @relationship.send(:remote_relationship)
126
135
  end
127
136
 
128
- def near_model
129
- @near_model ||= @parent.send(near_relationship_name)
137
+ def near_association
138
+ @near_association ||= @parent.send(near_relationship_name)
130
139
  end
131
140
 
132
141
  def near_relationship_name
@@ -27,7 +27,7 @@ module DataMapper
27
27
  def #{name}_association
28
28
  @#{name}_association ||= begin
29
29
  unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
31
  end
32
32
  association = Proxy.new(relationship, self)
33
33
  child_associations << association
@@ -39,7 +39,7 @@ module DataMapper
39
39
  model.relationships(repository_name)[name] = Relationship.new(
40
40
  name,
41
41
  repository_name,
42
- model.name,
42
+ model,
43
43
  options.fetch(:class_name, Extlib::Inflection.classify(name)),
44
44
  options
45
45
  )
@@ -48,7 +48,7 @@ module DataMapper
48
48
  class Proxy
49
49
  include Assertions
50
50
 
51
- instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not ].include?(m) }
51
+ instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m) }
52
52
 
53
53
  def replace(parent)
54
54
  @parent = parent
@@ -60,7 +60,7 @@ module DataMapper
60
60
  return false if @parent.nil?
61
61
  return true unless parent.new_record?
62
62
 
63
- DataMapper.repository(@relationship.repository_name) do
63
+ @relationship.with_repository(parent) do
64
64
  parent.save
65
65
  end
66
66
  end
@@ -1,5 +1,3 @@
1
- require 'forwardable'
2
-
3
1
  module DataMapper
4
2
  module Associations
5
3
  module OneToMany
@@ -29,7 +27,7 @@ module DataMapper
29
27
  def #{name}_association
30
28
  @#{name}_association ||= begin
31
29
  unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
32
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
33
31
  end
34
32
  association = Proxy.new(relationship, self)
35
33
  parent_associations << association
@@ -41,8 +39,14 @@ module DataMapper
41
39
  model.relationships(repository_name)[name] = if options.has_key?(:through)
42
40
  opts = options.dup
43
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
+
44
48
  opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
45
- opts[:parent_model] = model.name
49
+ opts[:parent_model] = model
46
50
  opts[:repository_name] = repository_name
47
51
  opts[:near_relationship_name] = opts.delete(:through)
48
52
  opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
@@ -52,10 +56,10 @@ module DataMapper
52
56
  RelationshipChain.new( opts )
53
57
  else
54
58
  Relationship.new(
55
- Extlib::Inflection.underscore(Extlib::Inflection.demodulize(model.name)).to_sym,
59
+ name,
56
60
  repository_name,
57
61
  options.fetch(:class_name, Extlib::Inflection.classify(name)),
58
- model.name,
62
+ model,
59
63
  options
60
64
  )
61
65
  end
@@ -68,7 +72,7 @@ module DataMapper
68
72
  class Proxy
69
73
  include Assertions
70
74
 
71
- instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not ].include?(m) }
75
+ instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m) }
72
76
 
73
77
  # FIXME: remove when RelationshipChain#get_children can return a Collection
74
78
  def all(query = {})
@@ -87,6 +91,7 @@ module DataMapper
87
91
 
88
92
  def <<(resource)
89
93
  assert_mutable
94
+ return self if !resource.new_record? && self.include?(resource)
90
95
  super
91
96
  relate_resource(resource)
92
97
  self
@@ -94,6 +99,7 @@ module DataMapper
94
99
 
95
100
  def push(*resources)
96
101
  assert_mutable
102
+ resources.reject { |resource| !resource.new_record? && self.include?(resource) }
97
103
  super
98
104
  resources.each { |resource| relate_resource(resource) }
99
105
  self
@@ -101,6 +107,7 @@ module DataMapper
101
107
 
102
108
  def unshift(*resources)
103
109
  assert_mutable
110
+ resources.reject { |resource| !resource.new_record? && self.include?(resource) }
104
111
  super
105
112
  resources.each { |resource| relate_resource(resource) }
106
113
  self
@@ -109,7 +116,7 @@ module DataMapper
109
116
  def replace(other)
110
117
  assert_mutable
111
118
  each { |resource| orphan_resource(resource) }
112
- other = other.map { |resource| Hash === resource ? @relationship.child_model.new(resource) : resource }
119
+ other = other.map { |resource| resource.kind_of?(Hash) ? new_child(resource) : resource }
113
120
  super
114
121
  other.each { |resource| relate_resource(resource) }
115
122
  self
@@ -142,10 +149,21 @@ module DataMapper
142
149
  self
143
150
  end
144
151
 
152
+ def build(attributes = {})
153
+ assert_mutable
154
+ attributes = default_attributes.merge(attributes)
155
+ resource = children.respond_to?(:build) ? super(attributes) : new_child(attributes)
156
+ self << resource
157
+ resource
158
+ end
159
+
145
160
  def create(attributes = {})
146
161
  assert_mutable
147
162
  raise UnsavedParentError, 'You cannot create until the parent is saved' if @parent.new_record?
148
- super
163
+ attributes = default_attributes.merge(attributes)
164
+ resource = children.respond_to?(:create) ? super(attributes) : @relationship.child_model.create(attributes)
165
+ self << resource
166
+ resource
149
167
  end
150
168
 
151
169
  def update(attributes = {})
@@ -229,15 +247,32 @@ module DataMapper
229
247
  raise ImmutableAssociationError, 'You can not modify this assocation' if children.frozen?
230
248
  end
231
249
 
232
- def add_default_association_values(resource)
233
- return if respond_to?(:default_attributes)
250
+ def default_attributes
251
+ default_attributes = {}
234
252
 
235
253
  @relationship.query.each do |attribute, value|
236
- next if Query::OPTIONS.include?(attribute) || attribute.kind_of?(Query::Operator) || resource.attribute_loaded?(attribute)
254
+ next if Query::OPTIONS.include?(attribute) || attribute.kind_of?(Query::Operator)
255
+ default_attributes[attribute] = value
256
+ end
257
+
258
+ @relationship.child_key.zip(@relationship.parent_key.get(@parent)) do |property,value|
259
+ default_attributes[property.name] = value
260
+ end
261
+
262
+ default_attributes
263
+ end
264
+
265
+ def add_default_association_values(resource)
266
+ default_attributes.each do |attribute, value|
267
+ next if !resource.respond_to?("#{attribute}=") || resource.attribute_loaded?(attribute)
237
268
  resource.send("#{attribute}=", value)
238
269
  end
239
270
  end
240
271
 
272
+ def new_child(attributes)
273
+ @relationship.child_model.new(default_attributes.merge(attributes))
274
+ end
275
+
241
276
  def relate_resource(resource)
242
277
  assert_mutable
243
278
  add_default_association_values(resource)
@@ -252,7 +287,7 @@ module DataMapper
252
287
  end
253
288
 
254
289
  def save_resource(resource, parent = @parent)
255
- DataMapper.repository(@relationship.repository_name) do
290
+ @relationship.with_repository(resource) do |r|
256
291
  if parent.nil? && resource.model.respond_to?(:many_to_many)
257
292
  resource.destroy
258
293
  else
@@ -27,7 +27,7 @@ module DataMapper
27
27
  def #{name}_association
28
28
  @#{name}_association ||= begin
29
29
  unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
- raise ArgumentError, 'Relationship #{name.inspect} does not exist'
30
+ raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
31
  end
32
32
  association = Associations::OneToMany::Proxy.new(relationship, self)
33
33
  parent_associations << association
@@ -39,7 +39,7 @@ module DataMapper
39
39
  model.relationships(repository_name)[name] = if options.has_key?(:through)
40
40
  RelationshipChain.new(
41
41
  :child_model => options.fetch(:class_name, Extlib::Inflection.classify(name)),
42
- :parent_model => model.name,
42
+ :parent_model => model,
43
43
  :repository_name => repository_name,
44
44
  :near_relationship_name => options[:through],
45
45
  :remote_relationship_name => options.fetch(:remote_name, name),
@@ -48,10 +48,10 @@ module DataMapper
48
48
  )
49
49
  else
50
50
  Relationship.new(
51
- Extlib::Inflection.underscore(Extlib::Inflection.demodulize(model.name)).to_sym,
51
+ name,
52
52
  repository_name,
53
53
  options.fetch(:class_name, Extlib::Inflection.classify(name)),
54
- model.name,
54
+ model,
55
55
  options
56
56
  )
57
57
  end
@@ -5,75 +5,166 @@ module DataMapper
5
5
 
6
6
  OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :through ]
7
7
 
8
- attr_reader :name, :repository_name, :options, :query
8
+ # @api private
9
+ attr_reader :name, :options, :query
9
10
 
11
+ # @api private
10
12
  def child_key
11
13
  @child_key ||= begin
12
- model_properties = child_model.properties(repository_name)
13
-
14
- child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
15
- # TODO: use something similar to DM::NamingConventions to determine the property name
16
- property_name ||= "#{name}_#{parent_property.name}".to_sym
17
-
18
- model_properties[property_name] || DataMapper.repository(repository_name) do
19
- attributes = {}
20
-
21
- [ :length, :precision, :scale ].each do |attribute|
22
- attributes[attribute] = parent_property.send(attribute)
23
- end
24
-
25
- # NOTE: hack to make each many to many child_key a true key,
26
- # until I can figure out a better place for this check
27
- if child_model.respond_to?(:many_to_many)
28
- attributes[:key] = true
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)
29
39
  end
30
-
31
- child_model.property(property_name, parent_property.primitive, attributes)
32
40
  end
33
41
  end
34
42
  PropertySet.new(child_key)
35
43
  end
36
44
  end
37
45
 
46
+ # @api private
38
47
  def parent_key
39
48
  @parent_key ||= begin
40
- parent_key = if @parent_properties
41
- parent_model.properties(repository_name).slice(*@parent_properties)
42
- else
43
- parent_model.key(repository_name)
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
44
56
  end
45
-
46
57
  PropertySet.new(parent_key)
47
58
  end
48
59
  end
49
60
 
61
+ # @api private
50
62
  def parent_model
51
- Class === @parent_model ? @parent_model : self.class.find_const(@parent_model)
63
+ Class === @parent_model ? @parent_model : (Class === @child_model ? @child_model.find_const(@parent_model) : Object.find_const(@parent_model))
64
+ rescue NameError
65
+ raise NameError, "Cannot find the parent_model #{@parent_model} for #{@child_model}"
52
66
  end
53
67
 
68
+ # @api private
54
69
  def child_model
55
- Class === @child_model ? @child_model : self.class.find_const(@child_model)
70
+ Class === @child_model ? @child_model : (Class === @parent_model ? @parent_model.find_const(@child_model) : Object.find_const(@child_model))
71
+ rescue NameError
72
+ raise NameError, "Cannot find the child_model #{@child_model} for #{@parent_model}"
56
73
  end
57
74
 
58
75
  # @api private
59
76
  def get_children(parent, options = {}, finder = :all, *args)
60
- bind_values = parent_key.get(parent)
61
- return [] if bind_values.any? { |bind_value| bind_value.nil? }
62
- query = child_key.to_query(bind_values)
77
+ bind_values = parent_key.get(parent)
78
+ parent_values = bind_values.dup
79
+
80
+ with_repository(child_model) do |r|
81
+ parent_identity_map = parent.repository.identity_map(parent_model)
82
+ child_identity_map = r.identity_map(child_model)
83
+
84
+ query_values = parent_identity_map.keys.flatten
85
+ query_values.reject! { |k| child_identity_map[[k]] }
86
+
87
+ bind_values = query_values unless query_values.empty?
88
+ query = child_key.map { |k| [ k, bind_values ] }.to_hash
89
+
90
+ collection = child_model.send(finder, *(args.dup << @query.merge(options).merge(query)))
91
+ return collection unless collection.kind_of?(Collection) && collection.any?
92
+
93
+ grouped_collection = Hash.new { |h,k| h[k] = [] }
94
+ collection.each do |resource|
95
+ grouped_collection[get_parent(resource, parent)] << resource
96
+ end
97
+
98
+ association_accessor = "#{self.name}_association"
99
+
100
+ ret = nil
101
+ grouped_collection.each do |parent, children|
102
+ association = parent.send(association_accessor)
103
+
104
+ query = collection.query.dup
105
+ query.conditions.map! do |operator, property, bind_value|
106
+ if child_key.has_property?(property.name)
107
+ bind_value = *children.map { |child| property.get(child) }.uniq
108
+ end
109
+ [ operator, property, bind_value ]
110
+ end
111
+
112
+ parents_children = Collection.new(query) do |collection|
113
+ children.each { |child| collection.send(:add, child) }
114
+ end
115
+
116
+ if parent_key.get(parent) == parent_values
117
+ ret = parents_children
118
+ else
119
+ association.instance_variable_set(:@children, parents_children)
120
+ end
121
+ end
63
122
 
64
- DataMapper.repository(repository_name) do
65
- child_model.send(finder, *(args << @query.merge(options).merge(query)))
123
+ ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip(parent_values).to_hash)))
66
124
  end
67
125
  end
68
126
 
69
127
  # @api private
70
- def get_parent(child)
71
- bind_values = child_key.get(child)
72
- return nil if bind_values.any? { |bind_value| bind_value.nil? }
73
- query = parent_key.to_query(bind_values)
128
+ def get_parent(child, parent = nil)
129
+ child_value = child_key.get(child)
130
+ return nil unless child_value.nitems == child_value.size
131
+
132
+ with_repository(parent || parent_model) do
133
+ parent_identity_map = (parent || parent_model).repository.identity_map(parent_model)
134
+ child_identity_map = child.repository.identity_map(child_model)
135
+
136
+ if parent = parent_identity_map[child_value]
137
+ return parent
138
+ end
139
+
140
+ children = child_identity_map.values
141
+
142
+ bind_values = *children.map { |c| child_key.get(c) }.uniq
143
+ query_values = bind_values.reject { |k| parent_identity_map[[k]] }
144
+
145
+ bind_values = query_values unless query_values.empty?
146
+ query = parent_key.map { |k| [ k, bind_values ] }.to_hash
74
147
 
75
- DataMapper.repository(repository_name) do
76
- parent_model.first(@query.merge(query))
148
+ association_accessor = "#{self.name}_association"
149
+
150
+ collection = parent_model.send(:all, query)
151
+ collection.send(:lazy_load)
152
+ children.each do |c|
153
+ c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
154
+ end
155
+ child.send(association_accessor).instance_variable_get(:@parent)
156
+ end
157
+ end
158
+
159
+ # @api private
160
+ def with_repository(object = nil, &block)
161
+ other_model = object.model == child_model ? parent_model : child_model if object.respond_to?(:model)
162
+ other_model = object == child_model ? parent_model : child_model if object.kind_of?(DataMapper::Resource)
163
+
164
+ if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
165
+ object.repository.scope(&block)
166
+ else
167
+ repository(@repository_name, &block)
77
168
  end
78
169
  end
79
170
 
@@ -89,10 +180,10 @@ module DataMapper
89
180
  # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
90
181
  # I wash my hands of it!
91
182
  def initialize(name, repository_name, child_model, parent_model, options = {})
92
- assert_kind_of 'name', name, Symbol
93
- assert_kind_of 'repository_name', repository_name, Symbol
94
- assert_kind_of 'child_model', child_model, String, Class
95
- assert_kind_of 'parent_model', parent_model, String, Class
183
+ assert_kind_of 'name', name, Symbol
184
+ assert_kind_of 'repository_name', repository_name, Symbol
185
+ assert_kind_of 'child_model', child_model, String, Class
186
+ assert_kind_of 'parent_model', parent_model, String, Class
96
187
 
97
188
  if child_properties = options[:child_key]
98
189
  assert_kind_of 'options[:child_key]', child_properties, Array
@@ -110,6 +201,17 @@ module DataMapper
110
201
  @parent_model = parent_model
111
202
  @parent_properties = parent_properties # may be nil
112
203
  @options = options
204
+
205
+ # attempt to load the child_key if the parent and child model constants are defined
206
+ if model_defined?(@child_model) && model_defined?(@parent_model)
207
+ child_key
208
+ end
209
+ end
210
+
211
+ # @api private
212
+ def model_defined?(model)
213
+ # TODO: figure out other ways to see if the model is loaded
214
+ model.kind_of?(Class)
113
215
  end
114
216
  end # class Relationship
115
217
  end # module Associations