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 (194) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -50
  5. data/Manifest.txt +66 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +6 -7
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/deps.rip +2 -0
  12. data/dm-core.gemspec +11 -15
  13. data/lib/dm-core.rb +105 -110
  14. data/lib/dm-core/adapters.rb +135 -16
  15. data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
  16. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  17. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  18. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  19. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  22. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  23. data/lib/dm-core/associations/many_to_many.rb +372 -90
  24. data/lib/dm-core/associations/many_to_one.rb +220 -73
  25. data/lib/dm-core/associations/one_to_many.rb +319 -255
  26. data/lib/dm-core/associations/one_to_one.rb +66 -53
  27. data/lib/dm-core/associations/relationship.rb +561 -156
  28. data/lib/dm-core/collection.rb +1101 -379
  29. data/lib/dm-core/core_ext/kernel.rb +12 -0
  30. data/lib/dm-core/core_ext/symbol.rb +10 -0
  31. data/lib/dm-core/identity_map.rb +4 -34
  32. data/lib/dm-core/migrations.rb +1283 -0
  33. data/lib/dm-core/model.rb +570 -369
  34. data/lib/dm-core/model/descendant_set.rb +81 -0
  35. data/lib/dm-core/model/hook.rb +45 -0
  36. data/lib/dm-core/model/is.rb +32 -0
  37. data/lib/dm-core/model/property.rb +247 -0
  38. data/lib/dm-core/model/relationship.rb +335 -0
  39. data/lib/dm-core/model/scope.rb +90 -0
  40. data/lib/dm-core/property.rb +808 -273
  41. data/lib/dm-core/property_set.rb +141 -98
  42. data/lib/dm-core/query.rb +1037 -483
  43. data/lib/dm-core/query/conditions/comparison.rb +872 -0
  44. data/lib/dm-core/query/conditions/operation.rb +221 -0
  45. data/lib/dm-core/query/direction.rb +43 -0
  46. data/lib/dm-core/query/operator.rb +84 -0
  47. data/lib/dm-core/query/path.rb +138 -0
  48. data/lib/dm-core/query/sort.rb +45 -0
  49. data/lib/dm-core/repository.rb +210 -94
  50. data/lib/dm-core/resource.rb +641 -421
  51. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  52. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  53. data/lib/dm-core/support/chainable.rb +22 -0
  54. data/lib/dm-core/support/deprecate.rb +12 -0
  55. data/lib/dm-core/support/logger.rb +13 -0
  56. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  57. data/lib/dm-core/transaction.rb +333 -92
  58. data/lib/dm-core/type.rb +98 -60
  59. data/lib/dm-core/types/boolean.rb +1 -1
  60. data/lib/dm-core/types/discriminator.rb +34 -20
  61. data/lib/dm-core/types/object.rb +7 -4
  62. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  63. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  64. data/lib/dm-core/types/serial.rb +3 -3
  65. data/lib/dm-core/types/text.rb +3 -4
  66. data/lib/dm-core/version.rb +1 -1
  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/migrations_spec.rb +359 -0
  80. data/spec/public/model/relationship_spec.rb +924 -0
  81. data/spec/public/model_spec.rb +159 -0
  82. data/spec/public/property_spec.rb +829 -0
  83. data/spec/public/resource_spec.rb +71 -0
  84. data/spec/public/sel_spec.rb +44 -0
  85. data/spec/public/setup_spec.rb +145 -0
  86. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  87. data/spec/public/shared/collection_shared_spec.rb +1670 -0
  88. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  89. data/spec/public/shared/resource_shared_spec.rb +924 -0
  90. data/spec/public/shared/sel_shared_spec.rb +112 -0
  91. data/spec/public/transaction_spec.rb +129 -0
  92. data/spec/public/types/discriminator_spec.rb +130 -0
  93. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  94. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  95. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  96. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  97. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  99. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  100. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  101. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  102. data/spec/semipublic/associations_spec.rb +177 -0
  103. data/spec/semipublic/collection_spec.rb +142 -0
  104. data/spec/semipublic/property_spec.rb +61 -0
  105. data/spec/semipublic/query/conditions_spec.rb +528 -0
  106. data/spec/semipublic/query/path_spec.rb +443 -0
  107. data/spec/semipublic/query_spec.rb +2626 -0
  108. data/spec/semipublic/resource_spec.rb +47 -0
  109. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  110. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  111. data/spec/spec.opts +3 -1
  112. data/spec/spec_helper.rb +80 -57
  113. data/tasks/ci.rb +19 -31
  114. data/tasks/dm.rb +43 -48
  115. data/tasks/doc.rb +8 -11
  116. data/tasks/gemspec.rb +5 -5
  117. data/tasks/hoe.rb +15 -16
  118. data/tasks/install.rb +8 -10
  119. metadata +74 -111
  120. data/lib/dm-core/associations.rb +0 -207
  121. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  122. data/lib/dm-core/auto_migrations.rb +0 -105
  123. data/lib/dm-core/dependency_queue.rb +0 -32
  124. data/lib/dm-core/hook.rb +0 -11
  125. data/lib/dm-core/is.rb +0 -16
  126. data/lib/dm-core/logger.rb +0 -232
  127. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  128. data/lib/dm-core/migrator.rb +0 -29
  129. data/lib/dm-core/scope.rb +0 -58
  130. data/lib/dm-core/support.rb +0 -7
  131. data/lib/dm-core/support/array.rb +0 -13
  132. data/lib/dm-core/support/assertions.rb +0 -8
  133. data/lib/dm-core/support/errors.rb +0 -23
  134. data/lib/dm-core/support/kernel.rb +0 -11
  135. data/lib/dm-core/support/symbol.rb +0 -41
  136. data/lib/dm-core/type_map.rb +0 -80
  137. data/lib/dm-core/types.rb +0 -19
  138. data/script/all +0 -4
  139. data/spec/integration/association_spec.rb +0 -1382
  140. data/spec/integration/association_through_spec.rb +0 -203
  141. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  142. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  143. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  144. data/spec/integration/auto_migrations_spec.rb +0 -413
  145. data/spec/integration/collection_spec.rb +0 -1073
  146. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  147. data/spec/integration/dependency_queue_spec.rb +0 -46
  148. data/spec/integration/model_spec.rb +0 -197
  149. data/spec/integration/mysql_adapter_spec.rb +0 -85
  150. data/spec/integration/postgres_adapter_spec.rb +0 -731
  151. data/spec/integration/property_spec.rb +0 -253
  152. data/spec/integration/query_spec.rb +0 -514
  153. data/spec/integration/repository_spec.rb +0 -61
  154. data/spec/integration/resource_spec.rb +0 -513
  155. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  156. data/spec/integration/sti_spec.rb +0 -273
  157. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  158. data/spec/integration/transaction_spec.rb +0 -75
  159. data/spec/integration/type_spec.rb +0 -275
  160. data/spec/lib/logging_helper.rb +0 -18
  161. data/spec/lib/mock_adapter.rb +0 -27
  162. data/spec/lib/model_loader.rb +0 -100
  163. data/spec/lib/publicize_methods.rb +0 -28
  164. data/spec/models/content.rb +0 -16
  165. data/spec/models/vehicles.rb +0 -34
  166. data/spec/models/zoo.rb +0 -48
  167. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  168. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  169. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  170. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  171. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  172. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  173. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  174. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  175. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  176. data/spec/unit/associations/relationship_spec.rb +0 -71
  177. data/spec/unit/associations_spec.rb +0 -242
  178. data/spec/unit/auto_migrations_spec.rb +0 -111
  179. data/spec/unit/collection_spec.rb +0 -182
  180. data/spec/unit/data_mapper_spec.rb +0 -35
  181. data/spec/unit/identity_map_spec.rb +0 -126
  182. data/spec/unit/is_spec.rb +0 -80
  183. data/spec/unit/migrator_spec.rb +0 -33
  184. data/spec/unit/model_spec.rb +0 -321
  185. data/spec/unit/naming_conventions_spec.rb +0 -36
  186. data/spec/unit/property_set_spec.rb +0 -90
  187. data/spec/unit/property_spec.rb +0 -753
  188. data/spec/unit/query_spec.rb +0 -571
  189. data/spec/unit/repository_spec.rb +0 -93
  190. data/spec/unit/resource_spec.rb +0 -649
  191. data/spec/unit/scope_spec.rb +0 -142
  192. data/spec/unit/transaction_spec.rb +0 -493
  193. data/spec/unit/type_map_spec.rb +0 -114
  194. data/spec/unit/type_spec.rb +0 -119
@@ -1,61 +1,74 @@
1
1
  module DataMapper
2
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
3
+ module OneToOne #:nodoc:
4
+ class Relationship < Associations::Relationship
5
+ %w[ public protected private ].map do |visibility|
6
+ superclass.send("#{visibility}_instance_methods", false).each do |method|
7
+ undef_method method unless method.to_s == 'initialize'
19
8
  end
9
+ end
20
10
 
21
- def #{name}=(child_resource)
22
- #{name}_association.replace(child_resource.nil? ? [] : [ child_resource ])
23
- end
11
+ # Loads (if necessary) and returns association target
12
+ # for given source
13
+ #
14
+ # @api semipublic
15
+ def get(source, other_query = nil)
16
+ assert_kind_of 'source', source, source_model
24
17
 
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
18
+ return unless loaded?(source) || source_key.get(source).all?
19
+
20
+ relationship.get(source, other_query).first
21
+ end
22
+
23
+ # Sets and returns association target
24
+ # for given source
25
+ #
26
+ # @api semipublic
27
+ def set(source, target)
28
+ assert_kind_of 'source', source, source_model
29
+ assert_kind_of 'target', target, target_model, Hash, NilClass
30
+
31
+ relationship.set(source, [ target ].compact).first
32
+ end
33
+
34
+ # TODO: document
35
+ # @api public
36
+ def kind_of?(klass)
37
+ super || relationship.kind_of?(klass)
38
+ end
39
+
40
+ # TODO: document
41
+ # @api public
42
+ def instance_of?(klass)
43
+ super || relationship.instance_of?(klass)
44
+ end
45
+
46
+ # TODO: document
47
+ # @api public
48
+ def respond_to?(method, include_private = false)
49
+ super || relationship.respond_to?(method, include_private)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :relationship
55
+
56
+ # Initializes the relationship. Always assumes target model class is
57
+ # a camel cased association name.
58
+ #
59
+ # @api semipublic
60
+ def initialize(name, target_model, source_model, options = {})
61
+ klass = options.key?(:through) ? ManyToMany::Relationship : OneToMany::Relationship
62
+ target_model ||= Extlib::Inflection.camelize(name).freeze
63
+ @relationship = klass.new(name, target_model, source_model, options)
64
+ end
65
+
66
+ # TODO: document
67
+ # @api private
68
+ def method_missing(method, *args, &block)
69
+ relationship.send(method, *args, &block)
70
+ end
71
+ end # class Relationship
59
72
  end # module HasOne
60
73
  end # module Associations
61
74
  end # module DataMapper
@@ -1,226 +1,631 @@
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)
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)
84
221
 
85
- query_values = parent_identity_map.keys
222
+ repository_name = parent_repository_name || child_repository_name
223
+ properties = parent_model.properties(repository_name)
86
224
 
87
- bind_values = query_values unless query_values.empty?
88
- 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
89
232
 
90
- 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
91
240
 
92
- 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
93
249
 
94
- grouped_collection = {}
95
- collection.each do |resource|
96
- child_value = child_key.get(resource)
97
- parent_obj = parent_identity_map[child_value]
98
- grouped_collection[parent_obj] ||= []
99
- grouped_collection[parent_obj] << resource
100
- 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
101
257
 
102
- 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
103
266
 
104
- ret = nil
105
- grouped_collection.each do |parent, children|
106
- 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] = [] }
107
280
 
108
- query = collection.query.dup
109
- query.conditions.map! do |operator, property, bind_value|
110
- if operator != :raw && child_key.has_property?(property.name)
111
- bind_value = *children.map { |child| property.get(child) }.uniq
112
- end
113
- [ operator, property, bind_value ]
114
- end
281
+ collection_query = query_for(source, query)
115
282
 
116
- parents_children = Collection.new(query)
117
- 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
118
286
 
119
- if parent_key.get(parent) == parent_value
120
- ret = parents_children
121
- else
122
- association.instance_variable_set(:@children, parents_children)
123
- end
124
- end
287
+ collection = source.model.all(collection_query).each do |target|
288
+ target_maps[target_key.get(target)] << target
289
+ end
125
290
 
126
- 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)
127
294
  end
295
+
296
+ collection
128
297
  end
129
298
 
130
- # @api private
131
- def get_parent(child, parent = nil)
132
- child_value = child_key.get(child)
133
- 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
134
305
 
135
- with_repository(parent || parent_model) do
136
- parent_identity_map = (parent || parent_model).repository.identity_map(parent_model.base_model)
137
- child_identity_map = child.repository.identity_map(child_model.base_model)
306
+ resource.instance_variable_defined?(instance_variable_name)
307
+ end
138
308
 
139
- if parent = parent_identity_map[child_value]
140
- return parent
141
- 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
142
328
 
143
- children = child_identity_map.values
144
- 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
145
342
 
146
- bind_values = children.map { |c| child_key.get(c) }.uniq
147
- 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
+
356
+ other.respond_to?(:cmp_repository?, true) &&
357
+ other.respond_to?(:cmp_model?, true) &&
358
+ other.respond_to?(:cmp_key?, true) &&
359
+ other.respond_to?(:query) &&
360
+ cmp?(other, :==)
361
+ end
148
362
 
149
- bind_values = query_values unless query_values.empty?
150
- query = parent_key.zip(bind_values.transpose).to_hash
151
- association_accessor = "#{self.name}_association"
363
+ # Get the inverse relationship from the target model
364
+ #
365
+ # @api semipublic
366
+ def inverse
367
+ return @inverse if defined?(@inverse)
152
368
 
153
- collection = parent_model.send(:all, query)
154
- unless collection.empty?
155
- collection.send(:lazy_load)
156
- children.each do |c|
157
- c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
158
- end
159
- child.send(association_accessor).instance_variable_get(:@parent)
160
- end
369
+ if kind_of_inverse?(options[:inverse])
370
+ return @inverse = options[:inverse]
161
371
  end
372
+
373
+ relationships = target_model.relationships(relative_target_repository_name).values
374
+
375
+ @inverse = relationships.detect { |relationship| inverse?(relationship) } ||
376
+ invert
377
+
378
+ @inverse.child_key
379
+
380
+ @inverse
162
381
  end
163
382
 
383
+ # TODO: document
164
384
  # @api private
165
- def with_repository(object = nil)
166
- other_model = object.model == child_model ? parent_model : child_model if object.respond_to?(:model)
167
- other_model = object == child_model ? parent_model : child_model if object.kind_of?(DataMapper::Resource)
385
+ def relative_target_repository_name
386
+ target_repository_name || source_repository_name
387
+ end
168
388
 
169
- if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
170
- object.repository.scope { |block_args| yield(*block_args) }
389
+ # TODO: document
390
+ # @api private
391
+ def relative_target_repository_name_for(source)
392
+ target_repository_name || if source.respond_to?(:repository)
393
+ source.repository.name
171
394
  else
172
- repository(@repository_name) { |block_args| yield(*block_args) }
395
+ source_repository_name
173
396
  end
174
397
  end
175
398
 
399
+ private
400
+
401
+ # TODO: document
176
402
  # @api private
177
- def attach_parent(child, parent)
178
- child_key.set(child, parent && parent_key.get(parent))
179
- end
403
+ attr_reader :child_properties
180
404
 
181
- private
405
+ # TODO: document
406
+ # @api private
407
+ attr_reader :parent_properties
408
+
409
+ # Initializes new Relationship: sets attributes of relationship
410
+ # from options as well as conventions: for instance, @ivar name
411
+ # for association is constructed by prefixing @ to association name.
412
+ #
413
+ # Once attributes are set, reader and writer are created for
414
+ # the resource association belongs to
415
+ #
416
+ # @api semipublic
417
+ def initialize(name, child_model, parent_model, options = {})
418
+ initialize_object_ivar('child_model', child_model)
419
+ initialize_object_ivar('parent_model', parent_model)
420
+
421
+ @name = name
422
+ @instance_variable_name = "@#{@name}".freeze
423
+ @options = options.dup.freeze
424
+ @child_repository_name = @options[:child_repository_name]
425
+ @parent_repository_name = @options[:parent_repository_name]
426
+ @child_properties = @options[:child_key].try_dup.freeze
427
+ @parent_properties = @options[:parent_key].try_dup.freeze
428
+ @min = @options[:min]
429
+ @max = @options[:max]
430
+
431
+ # TODO: normalize the @query to become :conditions => AndOperation
432
+ # - Property/Relationship/Path should be left alone
433
+ # - Symbol/String keys should become a Property, scoped to the target_repository and target_model
434
+ # - Extract subject (target) from Operator
435
+ # - subject should be processed same as above
436
+ # - each subject should be transformed into AbstractComparison
437
+ # object with the subject, operator and value
438
+ # - transform into an AndOperation object, and return the
439
+ # query as :condition => and_object from self.query
440
+ # - this should provide the best performance
441
+
442
+ @query = @options.except(*self.class::OPTIONS).freeze
443
+
444
+ create_reader
445
+ create_writer
446
+ end
182
447
 
183
- # +child_model_name and child_properties refers to the FK, parent_model_name
184
- # and parent_properties refer to the PK. For more information:
185
- # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
186
- # I wash my hands of it!
187
- def initialize(name, repository_name, child_model, parent_model, options = {})
188
- assert_kind_of 'name', name, Symbol
189
- assert_kind_of 'repository_name', repository_name, Symbol
190
- assert_kind_of 'child_model', child_model, String, Class
191
- assert_kind_of 'parent_model', parent_model, String, Class
192
-
193
- unless model_defined?(child_model) || model_defined?(parent_model)
194
- raise 'at least one of child_model and parent_model must be a Model object'
448
+ # Set the correct ivars for the named object
449
+ #
450
+ # This method should set the object in an ivar with the same name
451
+ # provided, plus it should set a String form of the object in
452
+ # a second ivar.
453
+ #
454
+ # @param [String]
455
+ # the name of the ivar to set
456
+ # @param [#name, #to_str, #to_sym] object
457
+ # the object to set in the ivar
458
+ #
459
+ # @return [String]
460
+ # the String value
461
+ #
462
+ # @raise [ArgumentError]
463
+ # raise when object does not respond to expected methods
464
+ #
465
+ # @api private
466
+ def initialize_object_ivar(name, object)
467
+ if object.respond_to?(:name)
468
+ instance_variable_set("@#{name}", object)
469
+ initialize_object_ivar(name, object.name)
470
+ elsif object.respond_to?(:to_str)
471
+ instance_variable_set("@#{name}_name", object.to_str.dup.freeze)
472
+ elsif object.respond_to?(:to_sym)
473
+ instance_variable_set("@#{name}_name", object.to_sym)
474
+ else
475
+ raise ArgumentError, "#{name} does not respond to #to_str or #name"
195
476
  end
196
477
 
197
- if child_properties = options[:child_key]
198
- assert_kind_of 'options[:child_key]', child_properties, Array
478
+ object
479
+ end
480
+
481
+ # Creates reader method for association.
482
+ #
483
+ # Must be implemented by subclasses.
484
+ #
485
+ # @api semipublic
486
+ def create_reader
487
+ raise NotImplementedError, "#{self.class}#create_reader not implemented"
488
+ end
489
+
490
+ # Creates both writer method for association.
491
+ #
492
+ # Must be implemented by subclasses.
493
+ #
494
+ # @api semipublic
495
+ def create_writer
496
+ raise NotImplementedError, "#{self.class}#create_writer not implemented"
497
+ end
498
+
499
+ # Sets the association targets in the resource
500
+ #
501
+ # @param [Resource] source
502
+ # the source to set
503
+ # @param [Array<Resource>] targets
504
+ # the targets for the association
505
+ # @param [Query, Hash] query
506
+ # the query to scope the association with
507
+ #
508
+ # @return [undefined]
509
+ #
510
+ # @api private
511
+ def eager_load_targets(source, targets, query)
512
+ raise NotImplementedError, "#{self.class}#eager_load_targets not implemented"
513
+ end
514
+
515
+ # TODO: document
516
+ # @api private
517
+ def valid_collection?(collection)
518
+ if collection.instance_of?(Array) || collection.loaded?
519
+ collection.all? { |resource| valid_resource?(resource) }
520
+ else
521
+ collection.model <= target_model && (collection.query.fields & target_key) == target_key
199
522
  end
523
+ end
524
+
525
+ # TODO: document
526
+ # @api private
527
+ def valid_resource?(resource)
528
+ resource.kind_of?(target_model) &&
529
+ target_key.zip(target_key.get!(resource)).all? { |property, value| property.valid?(value) }
530
+ end
531
+
532
+ # TODO: document
533
+ # @api private
534
+ def inverse?(other)
535
+ return true if @inverse.equal?(other)
536
+
537
+ other != self &&
538
+ kind_of_inverse?(other) &&
539
+ cmp_repository?(other, :==, :child) &&
540
+ cmp_repository?(other, :==, :parent) &&
541
+ cmp_model?(other, :==, :child) &&
542
+ cmp_model?(other, :==, :parent) &&
543
+ cmp_key?(other, :==, :child) &&
544
+ cmp_key?(other, :==, :parent)
545
+
546
+ # TODO: match only when the Query is empty, or is the same as the
547
+ # default scope for the target model
548
+ end
200
549
 
201
- if parent_properties = options[:parent_key]
202
- assert_kind_of 'options[:parent_key]', parent_properties, Array
550
+ # TODO: document
551
+ # @api private
552
+ def inverse_name
553
+ if options[:inverse].kind_of?(Relationship)
554
+ options[:inverse].name
555
+ else
556
+ options[:inverse]
203
557
  end
558
+ end
559
+
560
+ # TODO: document
561
+ # @api private
562
+ def invert
563
+ inverse_class.new(inverse_name, child_model, parent_model, inverted_options)
564
+ end
565
+
566
+ # TODO: document
567
+ # @api private
568
+ def inverted_options
569
+ options.only(*OPTIONS - [ :min, :max ]).update(:inverse => self)
570
+ end
204
571
 
205
- @name = name
206
- @repository_name = repository_name
207
- @child_model = child_model
208
- @child_properties = child_properties # may be nil
209
- @query = options.reject { |k,v| OPTIONS.include?(k) }
210
- @parent_model = parent_model
211
- @parent_properties = parent_properties # may be nil
212
- @options = options
213
-
214
- # attempt to load the child_key if the parent and child model constants are defined
215
- if model_defined?(@child_model) && model_defined?(@parent_model)
216
- child_key
572
+ # TODO: document
573
+ # @api private
574
+ def options_with_inverse
575
+ if child_model? && parent_model?
576
+ options.merge(:inverse => inverse)
577
+ else
578
+ options.merge(:inverse => inverse_name)
217
579
  end
218
580
  end
219
581
 
582
+ # TODO: document
583
+ # @api private
584
+ def kind_of_inverse?(other)
585
+ other.kind_of?(inverse_class)
586
+ end
587
+
588
+ # TODO: document
220
589
  # @api private
221
- def model_defined?(model)
222
- # TODO: figure out other ways to see if the model is loaded
223
- model.kind_of?(Class)
590
+ def cmp?(other, operator)
591
+ name.send(operator, other.name) &&
592
+ cmp_repository?(other, operator, :child) &&
593
+ cmp_repository?(other, operator, :parent) &&
594
+ cmp_model?(other, operator, :child) &&
595
+ cmp_model?(other, operator, :parent) &&
596
+ cmp_key?(other, operator, :child) &&
597
+ cmp_key?(other, operator, :parent) &&
598
+ query.send(operator, other.query)
599
+ end
600
+
601
+ # TODO: document
602
+ # @api private
603
+ def cmp_repository?(other, operator, type)
604
+ # if either repository is nil, then the relationship is relative,
605
+ # and the repositories are considered equivalent
606
+ return true unless repository_name = send("#{type}_repository_name")
607
+ return true unless other_repository_name = other.send("#{type}_repository_name")
608
+
609
+ repository_name.send(operator, other_repository_name)
610
+ end
611
+
612
+ # TODO: document
613
+ # @api private
614
+ def cmp_model?(other, operator, type)
615
+ send("#{type}_model?") &&
616
+ other.send("#{type}_model?") &&
617
+ send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model)
618
+ end
619
+
620
+ # TODO: document
621
+ # @api private
622
+ def cmp_key?(other, operator, type)
623
+ property_method = "#{type}_properties"
624
+
625
+ self_key = send(property_method)
626
+ other_key = other.send(property_method)
627
+
628
+ self_key.send(operator, other_key)
224
629
  end
225
630
  end # class Relationship
226
631
  end # module Associations