dm-core 0.9.11 → 0.10.0

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 (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