datamapper-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 (192) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -39
  5. data/Manifest.txt +67 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +16 -15
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/dm-core.gemspec +11 -15
  12. data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
  13. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  14. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  15. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  16. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  19. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  20. data/lib/dm-core/adapters.rb +135 -16
  21. data/lib/dm-core/associations/many_to_many.rb +372 -90
  22. data/lib/dm-core/associations/many_to_one.rb +220 -73
  23. data/lib/dm-core/associations/one_to_many.rb +319 -255
  24. data/lib/dm-core/associations/one_to_one.rb +66 -53
  25. data/lib/dm-core/associations/relationship.rb +560 -158
  26. data/lib/dm-core/collection.rb +1104 -381
  27. data/lib/dm-core/core_ext/kernel.rb +12 -0
  28. data/lib/dm-core/core_ext/symbol.rb +10 -0
  29. data/lib/dm-core/identity_map.rb +4 -34
  30. data/lib/dm-core/migrations.rb +1283 -0
  31. data/lib/dm-core/model/descendant_set.rb +81 -0
  32. data/lib/dm-core/model/hook.rb +45 -0
  33. data/lib/dm-core/model/is.rb +32 -0
  34. data/lib/dm-core/model/property.rb +248 -0
  35. data/lib/dm-core/model/relationship.rb +335 -0
  36. data/lib/dm-core/model/scope.rb +90 -0
  37. data/lib/dm-core/model.rb +570 -369
  38. data/lib/dm-core/property.rb +753 -280
  39. data/lib/dm-core/property_set.rb +141 -98
  40. data/lib/dm-core/query/conditions/comparison.rb +814 -0
  41. data/lib/dm-core/query/conditions/operation.rb +247 -0
  42. data/lib/dm-core/query/direction.rb +43 -0
  43. data/lib/dm-core/query/operator.rb +42 -0
  44. data/lib/dm-core/query/path.rb +102 -0
  45. data/lib/dm-core/query/sort.rb +45 -0
  46. data/lib/dm-core/query.rb +974 -492
  47. data/lib/dm-core/repository.rb +147 -107
  48. data/lib/dm-core/resource.rb +644 -429
  49. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  50. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  51. data/lib/dm-core/support/chainable.rb +20 -0
  52. data/lib/dm-core/support/deprecate.rb +12 -0
  53. data/lib/dm-core/support/equalizer.rb +23 -0
  54. data/lib/dm-core/support/logger.rb +13 -0
  55. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  56. data/lib/dm-core/transaction.rb +333 -92
  57. data/lib/dm-core/type.rb +98 -60
  58. data/lib/dm-core/types/boolean.rb +1 -1
  59. data/lib/dm-core/types/discriminator.rb +34 -20
  60. data/lib/dm-core/types/object.rb +7 -4
  61. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  62. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  63. data/lib/dm-core/types/serial.rb +3 -3
  64. data/lib/dm-core/types/text.rb +3 -4
  65. data/lib/dm-core/version.rb +1 -1
  66. data/lib/dm-core.rb +106 -110
  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/model/relationship_spec.rb +924 -0
  80. data/spec/public/model_spec.rb +159 -0
  81. data/spec/public/property_spec.rb +829 -0
  82. data/spec/public/resource_spec.rb +71 -0
  83. data/spec/public/sel_spec.rb +44 -0
  84. data/spec/public/setup_spec.rb +145 -0
  85. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  86. data/spec/public/shared/collection_shared_spec.rb +1723 -0
  87. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  88. data/spec/public/shared/resource_shared_spec.rb +924 -0
  89. data/spec/public/shared/sel_shared_spec.rb +112 -0
  90. data/spec/public/transaction_spec.rb +129 -0
  91. data/spec/public/types/discriminator_spec.rb +130 -0
  92. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  93. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  94. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  95. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  96. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  97. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  99. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  100. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  101. data/spec/semipublic/associations_spec.rb +177 -0
  102. data/spec/semipublic/collection_spec.rb +142 -0
  103. data/spec/semipublic/property_spec.rb +61 -0
  104. data/spec/semipublic/query/conditions_spec.rb +528 -0
  105. data/spec/semipublic/query/path_spec.rb +443 -0
  106. data/spec/semipublic/query_spec.rb +2626 -0
  107. data/spec/semipublic/resource_spec.rb +47 -0
  108. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  109. data/spec/spec.opts +3 -1
  110. data/spec/spec_helper.rb +80 -57
  111. data/tasks/ci.rb +19 -31
  112. data/tasks/dm.rb +43 -48
  113. data/tasks/doc.rb +8 -11
  114. data/tasks/gemspec.rb +5 -5
  115. data/tasks/hoe.rb +15 -16
  116. data/tasks/install.rb +8 -10
  117. metadata +72 -93
  118. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  119. data/lib/dm-core/associations.rb +0 -207
  120. data/lib/dm-core/auto_migrations.rb +0 -105
  121. data/lib/dm-core/dependency_queue.rb +0 -32
  122. data/lib/dm-core/hook.rb +0 -11
  123. data/lib/dm-core/is.rb +0 -16
  124. data/lib/dm-core/logger.rb +0 -232
  125. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  126. data/lib/dm-core/migrator.rb +0 -29
  127. data/lib/dm-core/scope.rb +0 -58
  128. data/lib/dm-core/support/array.rb +0 -13
  129. data/lib/dm-core/support/assertions.rb +0 -8
  130. data/lib/dm-core/support/errors.rb +0 -23
  131. data/lib/dm-core/support/kernel.rb +0 -11
  132. data/lib/dm-core/support/symbol.rb +0 -41
  133. data/lib/dm-core/support.rb +0 -7
  134. data/lib/dm-core/type_map.rb +0 -80
  135. data/lib/dm-core/types.rb +0 -19
  136. data/script/all +0 -4
  137. data/spec/integration/association_spec.rb +0 -1382
  138. data/spec/integration/association_through_spec.rb +0 -203
  139. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  140. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  141. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  142. data/spec/integration/auto_migrations_spec.rb +0 -413
  143. data/spec/integration/collection_spec.rb +0 -1073
  144. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  145. data/spec/integration/dependency_queue_spec.rb +0 -46
  146. data/spec/integration/model_spec.rb +0 -197
  147. data/spec/integration/mysql_adapter_spec.rb +0 -85
  148. data/spec/integration/postgres_adapter_spec.rb +0 -731
  149. data/spec/integration/property_spec.rb +0 -253
  150. data/spec/integration/query_spec.rb +0 -514
  151. data/spec/integration/repository_spec.rb +0 -61
  152. data/spec/integration/resource_spec.rb +0 -513
  153. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  154. data/spec/integration/sti_spec.rb +0 -273
  155. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  156. data/spec/integration/transaction_spec.rb +0 -75
  157. data/spec/integration/type_spec.rb +0 -275
  158. data/spec/lib/logging_helper.rb +0 -18
  159. data/spec/lib/mock_adapter.rb +0 -27
  160. data/spec/lib/model_loader.rb +0 -100
  161. data/spec/lib/publicize_methods.rb +0 -28
  162. data/spec/models/content.rb +0 -16
  163. data/spec/models/vehicles.rb +0 -34
  164. data/spec/models/zoo.rb +0 -48
  165. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  166. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  167. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  168. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  169. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  170. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  171. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  172. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  173. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  174. data/spec/unit/associations/relationship_spec.rb +0 -71
  175. data/spec/unit/associations_spec.rb +0 -242
  176. data/spec/unit/auto_migrations_spec.rb +0 -111
  177. data/spec/unit/collection_spec.rb +0 -182
  178. data/spec/unit/data_mapper_spec.rb +0 -35
  179. data/spec/unit/identity_map_spec.rb +0 -126
  180. data/spec/unit/is_spec.rb +0 -80
  181. data/spec/unit/migrator_spec.rb +0 -33
  182. data/spec/unit/model_spec.rb +0 -321
  183. data/spec/unit/naming_conventions_spec.rb +0 -36
  184. data/spec/unit/property_set_spec.rb +0 -90
  185. data/spec/unit/property_spec.rb +0 -753
  186. data/spec/unit/query_spec.rb +0 -571
  187. data/spec/unit/repository_spec.rb +0 -93
  188. data/spec/unit/resource_spec.rb +0 -649
  189. data/spec/unit/scope_spec.rb +0 -142
  190. data/spec/unit/transaction_spec.rb +0 -493
  191. data/spec/unit/type_map_spec.rb +0 -114
  192. data/spec/unit/type_spec.rb +0 -119
data/lib/dm-core/query.rb CHANGED
@@ -1,139 +1,525 @@
1
+ # TODO: break this up into classes for each primary option, eg:
2
+ #
3
+ # - DataMapper::Query::Fields
4
+ # - DataMapper::Query::Links
5
+ # - DataMapper::Query::Conditions
6
+ # - DataMapper::Query::Offset
7
+ # - DataMapper::Query::Limit
8
+ # - DataMapper::Query::Order
9
+ #
10
+ # TODO: move assertions, validations, transformations, and equality
11
+ # checking into each class and clean up Query
12
+ #
13
+ # TODO: add a way to "register" these classes with the Query object
14
+ # so that new reserved options can be added in the future. Each
15
+ # class will need to implement a "slug" method or something similar
16
+ # so that their option namespace can be reserved.
17
+
18
+ # TODO: move condition transformations into a Query::Conditions
19
+ # helper class that knows how to transform the primitives, and
20
+ # calls #comparison_for(repository, model) on objects (or some
21
+ # other convention that we establish)
22
+
1
23
  module DataMapper
24
+
25
+ # Query class represents a query which will be run against the data-store.
26
+ # Generally Query objects can be found inside Collection objects.
27
+ #
2
28
  class Query
3
- include Assertions
29
+ include Extlib::Assertions
30
+ extend Equalizer
31
+
32
+ OPTIONS = [ :fields, :links, :conditions, :offset, :limit, :order, :unique, :add_reversed, :reload ].to_set.freeze
33
+
34
+ equalize :repository, :model, :sorted_fields, :links, :conditions, :order, :offset, :limit, :reload?, :unique?, :add_reversed?
35
+
36
+ # Extract conditions to match a Resource or Collection
37
+ #
38
+ # @param [Array, Collection, Resource] source
39
+ # the source to extract the values from
40
+ # @param [ProperySet] source_key
41
+ # the key to extract the value from the resource
42
+ # @param [ProperySet] target_key
43
+ # the key to match the resource with
44
+ #
45
+ # @return [AbstractComparison, AbstractOperation]
46
+ # the conditions to match the resources with
47
+ #
48
+ # @api private
49
+ def self.target_conditions(source, source_key, target_key)
50
+ source_values = []
51
+
52
+ if source.nil?
53
+ source_values << [ nil ] * target_key.size
54
+ else
55
+ Array(source).each do |resource|
56
+ next unless source_key.loaded?(resource)
57
+ source_values << source_key.get!(resource)
58
+ end
59
+ end
60
+
61
+ source_values.uniq!
62
+
63
+ if target_key.size == 1
64
+ target_key = target_key.first
65
+ source_values.flatten!
66
+
67
+ if source_values.size == 1
68
+ Conditions::EqualToComparison.new(target_key, source_values.first)
69
+ else
70
+ Conditions::InclusionComparison.new(target_key, source_values)
71
+ end
72
+ else
73
+ or_operation = Conditions::OrOperation.new
74
+
75
+ source_values.each do |source_value|
76
+ and_operation = Conditions::AndOperation.new
77
+
78
+ target_key.zip(source_value) do |property, value|
79
+ and_operation << Conditions::EqualToComparison.new(property, value)
80
+ end
81
+
82
+ or_operation << and_operation
83
+ end
84
+
85
+ or_operation
86
+ end
87
+ end
88
+
89
+ # Returns the repository query should be
90
+ # executed in
91
+ #
92
+ # Set in cases like the following:
93
+ #
94
+ # @example
95
+ #
96
+ # Document.all(:repository => :medline)
97
+ #
98
+ #
99
+ # @return [Repository]
100
+ # the Repository to retrieve results from
101
+ #
102
+ # @api semipublic
103
+ attr_reader :repository
104
+
105
+ # Returns model (class) that is used
106
+ # to instantiate objects from query result
107
+ # returned by adapter
108
+ #
109
+ # @return [Model]
110
+ # the Model to retrieve results from
111
+ #
112
+ # @api semipublic
113
+ attr_reader :model
114
+
115
+ # Returns the fields
116
+ #
117
+ # Set in cases like the following:
118
+ #
119
+ # @example
120
+ #
121
+ # Document.all(:fields => [:title, :vernacular_title, :abstract])
122
+ #
123
+ # @return [PropertySet]
124
+ # the properties in the Model that will be retrieved
125
+ #
126
+ # @api semipublic
127
+ attr_reader :fields
128
+
129
+ # Returns the links (associations) query fetches
130
+ #
131
+ # @return [Array<DataMapper::Associations::Relationship>]
132
+ # the relationships that will be used to scope the results
133
+ #
134
+ # @api private
135
+ attr_reader :links
136
+
137
+ # Returns the conditions of the query
138
+ #
139
+ # In the following example:
140
+ #
141
+ # @example
142
+ #
143
+ # Team.all(:wins.gt => 30, :conference => 'East')
144
+ #
145
+ # Conditions are "greater than" operator for "wins"
146
+ # field and exact match operator for "conference".
147
+ #
148
+ # @return [Array]
149
+ # the conditions that will be used to scope the results
150
+ #
151
+ # @api semipublic
152
+ attr_reader :conditions
4
153
 
5
- OPTIONS = [
6
- :reload, :offset, :limit, :order, :add_reversed, :fields, :links, :includes, :conditions, :unique
7
- ]
154
+ # Returns the offset query uses
155
+ #
156
+ # Set in cases like the following:
157
+ #
158
+ # @example
159
+ #
160
+ # Document.all(:offset => page.offset)
161
+ #
162
+ # @return [Integer]
163
+ # the offset of the results
164
+ #
165
+ # @api semipublic
166
+ attr_reader :offset
167
+
168
+ # Returns the limit query uses
169
+ #
170
+ # Set in cases like the following:
171
+ #
172
+ # @example
173
+ #
174
+ # Document.all(:limit => 10)
175
+ #
176
+ # @return [Integer, NilClass]
177
+ # the maximum number of results
178
+ #
179
+ # @api semipublic
180
+ attr_reader :limit
181
+
182
+ # Returns the order
183
+ #
184
+ # Set in cases like the following:
185
+ #
186
+ # @example
187
+ #
188
+ # Document.all(:order => [:created_at.desc, :length.desc])
189
+ #
190
+ # query order is a set of two ordering rules, descending on
191
+ # "created_at" field and descending again on "length" field
192
+ #
193
+ # @return [Array]
194
+ # the order of results
195
+ #
196
+ # @api semipublic
197
+ attr_reader :order
198
+
199
+ # Returns the original options
200
+ #
201
+ # @return [Hash]
202
+ # the original options
203
+ #
204
+ # @api private
205
+ attr_reader :options
8
206
 
9
- attr_reader :repository, :model, *OPTIONS - [ :reload, :unique ]
10
- attr_writer :add_reversed
11
- alias add_reversed? add_reversed
207
+ # Indicates if each result should be returned in reverse order
208
+ #
209
+ # Set in cases like the following:
210
+ #
211
+ # @example
212
+ #
213
+ # Document.all(:limit => 5).reverse
214
+ #
215
+ # Note that :add_reversed option may be used in conditions directly,
216
+ # but this is rarely the case
217
+ #
218
+ # @return [Boolean]
219
+ # true if the results should be reversed, false if not
220
+ #
221
+ # @api private
222
+ def add_reversed?
223
+ @add_reversed
224
+ end
12
225
 
226
+ # Indicates if the Query results should replace the results in the Identity Map
227
+ #
228
+ # TODO: needs example
229
+ #
230
+ # @return [Boolean]
231
+ # true if the results should be reloaded, false if not
232
+ #
233
+ # @api semipublic
13
234
  def reload?
14
235
  @reload
15
236
  end
16
237
 
238
+ # Indicates if the Query results should be unique
239
+ #
240
+ # TODO: needs example
241
+ #
242
+ # @return [Boolean]
243
+ # true if the results should be unique, false if not
244
+ #
245
+ # @api semipublic
17
246
  def unique?
18
247
  @unique
19
248
  end
20
249
 
250
+ # Indicates if the Query has raw conditions
251
+ #
252
+ # @return [Boolean]
253
+ # true if the query has raw conditions, false if not
254
+ #
255
+ # @api semipublic
256
+ def raw?
257
+ @raw
258
+ end
259
+
260
+ # Indicates if the Query is valid
261
+ #
262
+ # @return [Boolean]
263
+ # true if the query is valid
264
+ #
265
+ # @api semipublic
266
+ def valid?
267
+ conditions.valid?
268
+ end
269
+
270
+ # Returns a new Query with a reversed order
271
+ #
272
+ # @example
273
+ #
274
+ # Document.all(:limit => 5).reverse
275
+ #
276
+ # Will execute a single query with correct order
277
+ #
278
+ # @return [Query]
279
+ # new Query with reversed order
280
+ #
281
+ # @api semipublic
21
282
  def reverse
22
283
  dup.reverse!
23
284
  end
24
285
 
286
+ # Reverses the sort order of the Query
287
+ #
288
+ # @example
289
+ #
290
+ # Document.all(:limit => 5).reverse
291
+ #
292
+ # Will execute a single query with original order
293
+ # and then reverse collection in the Ruby space
294
+ #
295
+ # @return [Query]
296
+ # self
297
+ #
298
+ # @api semipublic
25
299
  def reverse!
26
300
  # reverse the sort order
27
- update(:order => self.order.map { |o| o.reverse })
301
+ @order.map! { |direction| direction.reverse! }
302
+
303
+ # copy the order to the options
304
+ @options = @options.merge(:order => @order.map { |direction| direction.dup }).freeze
28
305
 
29
306
  self
30
307
  end
31
308
 
309
+ # Updates the Query with another Query or conditions
310
+ #
311
+ # Pretty unrealistic example:
312
+ #
313
+ # @example
314
+ #
315
+ # Journal.all(:limit => 2).query.limit # => 2
316
+ # Journal.all(:limit => 2).query.update(:limit => 3).limit # => 3
317
+ #
318
+ # @param [Query, Hash] other
319
+ # other Query or conditions
320
+ #
321
+ # @return [Query]
322
+ # self
323
+ #
324
+ # @api semipublic
32
325
  def update(other)
33
326
  assert_kind_of 'other', other, self.class, Hash
34
327
 
35
- assert_valid_other(other)
36
-
37
- if other.kind_of?(Hash)
38
- return self if other.empty?
39
- other = self.class.new(@repository, model, other)
328
+ other_options = if other.kind_of? self.class
329
+ if self.eql?(other)
330
+ return self
331
+ end
332
+ assert_valid_other(other)
333
+ other.options
334
+ else
335
+ other
40
336
  end
41
337
 
42
- return self if self == other
43
-
44
- # TODO: update this so if "other" had a value explicitly set
45
- # overwrite the attributes in self
46
-
47
- # only overwrite the attributes with non-default values
48
- @reload = other.reload? unless other.reload? == false
49
- @unique = other.unique? unless other.unique? == false
50
- @offset = other.offset if other.reload? || other.offset != 0
51
- @limit = other.limit unless other.limit == nil
52
- @order = other.order unless other.order == model.default_order
53
- @add_reversed = other.add_reversed? unless other.add_reversed? == false
54
- @fields = other.fields unless other.fields == @properties.defaults
55
- @links = other.links unless other.links == []
56
- @includes = other.includes unless other.includes == []
57
-
58
- update_conditions(other)
338
+ unless other_options.empty?
339
+ options = @options.merge(other_options)
340
+ if @options[:conditions] and other_options[:conditions]
341
+ options[:conditions] = @options[:conditions].dup << other_options[:conditions]
342
+ end
343
+ initialize(repository, model, options)
344
+ end
59
345
 
60
346
  self
61
347
  end
62
348
 
349
+ # Similar to Query#update, but acts on a duplicate.
350
+ #
351
+ # @param [Query, Hash] other
352
+ # other query to merge with
353
+ #
354
+ # @return [Query]
355
+ # updated duplicate of original query
356
+ #
357
+ # @api semipublic
63
358
  def merge(other)
64
359
  dup.update(other)
65
360
  end
66
361
 
67
- def ==(other)
68
- return true if super
69
- return false unless other.kind_of?(self.class)
70
-
71
- # TODO: add a #hash method, and then use it in the comparison, eg:
72
- # return hash == other.hash
73
- @model == other.model &&
74
- @reload == other.reload? &&
75
- @unique == other.unique? &&
76
- @offset == other.offset &&
77
- @limit == other.limit &&
78
- @order == other.order && # order is significant, so do not sort this
79
- @add_reversed == other.add_reversed? &&
80
- @fields == other.fields && # TODO: sort this so even if the order is different, it is equal
81
- @links == other.links && # TODO: sort this so even if the order is different, it is equal
82
- @includes == other.includes && # TODO: sort this so even if the order is different, it is equal
83
- @conditions.sort_by { |c| c.at(0).hash + c.at(1).hash + c.at(2).hash } == other.conditions.sort_by { |c| c.at(0).hash + c.at(1).hash + c.at(2).hash }
84
- end
85
-
86
- alias eql? ==
87
-
88
- def bind_values
89
- bind_values = []
90
- conditions.each do |tuple|
91
- next if tuple.size == 2
92
- operator, property, bind_value = *tuple
93
- if :raw == operator
94
- bind_values.push(*bind_value)
95
- else
96
- bind_values << bind_value
97
- end
362
+ # Builds and returns new query that merges
363
+ # original with one given, and slices the result
364
+ # with respect to :limit and :offset options
365
+ #
366
+ # This method is used by Collection to
367
+ # concatenate options from multiple chained
368
+ # calls in cases like the following:
369
+ #
370
+ # @example
371
+ #
372
+ # author.books.all(:year => 2009).all(:published => false)
373
+ #
374
+ # @api semipublic
375
+ def relative(options)
376
+ assert_kind_of 'options', options, Hash
377
+
378
+ options = options.dup
379
+
380
+ repository = options.delete(:repository) || self.repository
381
+
382
+ if repository.kind_of?(Symbol)
383
+ repository = DataMapper.repository(repository)
98
384
  end
99
- bind_values
100
- end
101
385
 
102
- def inheritance_property
103
- fields.detect { |property| property.type == DataMapper::Types::Discriminator }
386
+ if options.key?(:offset) && (options.key?(:limit) || self.limit)
387
+ offset = options.delete(:offset)
388
+ limit = options.delete(:limit) || self.limit - offset
389
+
390
+ self.class.new(repository, model, @options.merge(options)).slice!(offset, limit)
391
+ else
392
+ self.class.new(repository, model, @options.merge(options))
393
+ end
104
394
  end
105
395
 
106
- def inheritance_property_index
107
- fields.index(inheritance_property)
396
+ # Takes an Enumerable of records, and destructively filters it.
397
+ # First finds all matching conditions, then sorts it,
398
+ # then does offset & limit
399
+ #
400
+ # @param [Enumerable] records
401
+ # The set of records to be filtered
402
+ #
403
+ # @return [Enumerable]
404
+ # Whats left of the given array after the filtering
405
+ #
406
+ # @api semipublic
407
+ def filter_records(records)
408
+ records = records.uniq if unique?
409
+ records = match_records(records)
410
+ records = sort_records(records)
411
+ records = limit_records(records)
412
+ records
108
413
  end
109
414
 
110
- # TODO: spec this
111
- def key_property_indexes(repository)
112
- if (key_property_indexes = model.key(repository.name).map { |property| fields.index(property) }).all?
113
- key_property_indexes
415
+ # Filter a set of records by the conditions
416
+ #
417
+ # @param [Enumerable] records
418
+ # The set of records to be filtered
419
+ #
420
+ # @return [Enumerable]
421
+ # Whats left of the given array after the matching
422
+ #
423
+ # @api semipublic
424
+ def match_records(records)
425
+ return records if conditions.nil?
426
+ records.select do |record|
427
+ conditions.matches?(record)
114
428
  end
115
429
  end
116
430
 
117
- # find the point in self.conditions where the sub select tuple is
118
- # located. Delete the tuple and add value.conditions. value must be a
119
- # <DM::Query>
431
+ # Sorts a list of Records by the order
432
+ #
433
+ # @param [Enumerable] records
434
+ # A list of Resources to sort
120
435
  #
121
- def merge_subquery(operator, property, value)
122
- assert_kind_of 'value', value, self.class
436
+ # @return [Enumerable]
437
+ # The sorted records
438
+ #
439
+ # @api semipublic
440
+ def sort_records(records)
441
+ sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
123
442
 
124
- new_conditions = []
125
- conditions.each do |tuple|
126
- if tuple.at(0).to_s == operator.to_s && tuple.at(1) == property && tuple.at(2) == value
127
- value.conditions.each do |subquery_tuple|
128
- new_conditions << subquery_tuple
129
- end
130
- else
131
- new_conditions << tuple
443
+ records.sort_by do |record|
444
+ sort_order.map do |(property, ascending)|
445
+ Sort.new(record_value(record, property), ascending)
132
446
  end
133
447
  end
134
- @conditions = new_conditions
135
448
  end
136
449
 
450
+ # Limits a set of records by the offset and/or limit
451
+ #
452
+ # @param [Enumerable] records
453
+ # A list of Recrods to sort
454
+ #
455
+ # @return [Enumerable]
456
+ # The offset & limited records
457
+ #
458
+ # @api semipublic
459
+ def limit_records(records)
460
+ size = records.size
461
+
462
+ if offset > size - 1
463
+ []
464
+ elsif (limit && limit != size) || offset > 0
465
+ records[offset, limit || size] || []
466
+ else
467
+ records.dup
468
+ end
469
+ end
470
+
471
+ # Slices collection by adding limit and offset to the
472
+ # query, so a single query is executed
473
+ #
474
+ # @example
475
+ #
476
+ # Journal.all(:limit => 10).slice(3, 5)
477
+ #
478
+ # will execute query with the following limit and offset
479
+ # (when repository uses DataObjects adapter, and thus
480
+ # queries use SQL):
481
+ #
482
+ # LIMIT 5 OFFSET 3
483
+ #
484
+ # @api semipublic
485
+ def slice(*args)
486
+ dup.slice!(*args)
487
+ end
488
+
489
+ alias [] slice
490
+
491
+ # Slices collection by adding limit and offset to the
492
+ # query, so a single query is executed
493
+ #
494
+ # @example
495
+ #
496
+ # Journal.all(:limit => 10).slice!(3, 5)
497
+ #
498
+ # will execute query with the following limit
499
+ # (when repository uses DataObjects adapter, and thus
500
+ # queries use SQL):
501
+ #
502
+ # LIMIT 10
503
+ #
504
+ # and then takes a slice of collection in the Ruby space
505
+ #
506
+ # @api semipublic
507
+ def slice!(*args)
508
+ offset, limit = extract_slice_arguments(*args)
509
+
510
+ if self.limit || self.offset > 0
511
+ offset, limit = get_relative_position(offset, limit)
512
+ end
513
+
514
+ update(:offset => offset, :limit => limit)
515
+ end
516
+
517
+ # Returns detailed human readable
518
+ # string representation of the query
519
+ #
520
+ # @return [String] detailed string representation of the query
521
+ #
522
+ # @api semipublic
137
523
  def inspect
138
524
  attrs = [
139
525
  [ :repository, repository.name ],
@@ -148,529 +534,625 @@ module DataMapper
148
534
  [ :unique, unique? ],
149
535
  ]
150
536
 
151
- "#<#{self.class.name} #{attrs.map { |(k,v)| "@#{k}=#{v.inspect}" } * ' '}>"
152
- end
153
-
154
- # TODO: add docs
155
- # @api public
156
- def to_hash
157
- hash = {
158
- :reload => reload?,
159
- :unique => unique?,
160
- :offset => offset,
161
- :order => order,
162
- :add_reversed => add_reversed?,
163
- :fields => fields,
164
- }
165
-
166
- hash[:limit] = limit unless limit == nil
167
- hash[:links] = links unless links == []
168
- hash[:includes] = includes unless includes == []
169
-
170
- conditions = {}
171
- raw_queries = []
172
- bind_values = []
173
-
174
- conditions.each do |condition|
175
- if condition[0] == :raw
176
- raw_queries << condition[1]
177
- bind_values << condition[2]
178
- else
179
- operator, property, bind_value = condition
180
- conditions[ Query::Operator.new(property, operator) ] = bind_value
181
- end
182
- end
183
-
184
- if raw_queries.any?
185
- hash[:conditions] = [ raw_queries.join(' ') ].concat(bind_values)
186
- end
187
-
188
- hash.update(conditions)
537
+ "#<#{self.class.name} #{attrs.map { |key, value| "@#{key}=#{value.inspect}" }.join(' ')}>"
189
538
  end
190
539
 
191
- # TODO: add docs
540
+ # Get the properties used in the conditions
541
+ #
542
+ # @return [Set<Property>]
543
+ # Set of properties used in the conditions
544
+ #
192
545
  # @api private
193
- def _dump(*)
194
- Marshal.dump([ repository, model, to_hash ])
546
+ def condition_properties
547
+ properties = Set.new
548
+
549
+ each_comparison do |comparison|
550
+ properties << comparison.subject if comparison.subject.kind_of?(Property)
551
+ end
552
+
553
+ properties
195
554
  end
196
555
 
197
- # TODO: add docs
556
+ # Return a list of fields in predictable order
557
+ #
558
+ # @return [Array<Property>]
559
+ # list of fields sorted in deterministic order
560
+ #
198
561
  # @api private
199
- def self._load(marshalled)
200
- new(*Marshal.load(marshalled))
562
+ def sorted_fields
563
+ fields.sort_by { |property| property.hash }
201
564
  end
202
565
 
203
566
  private
204
567
 
568
+ # Initializes a Query instance
569
+ #
570
+ # @example
571
+ #
572
+ # JournalIssue.all(:repository => :medline, :created_on.gte => Date.today - 7)
573
+ #
574
+ # initialized a query with repository defined with name :medline,
575
+ # model JournalIssue and options { :created_on.gte => Date.today - 7 }
576
+ #
577
+ # @param [Repository] repository
578
+ # the Repository to retrieve results from
579
+ # @param [Model] model
580
+ # the Model to retrieve results from
581
+ # @param [Hash] options
582
+ # the conditions and scope
583
+ #
584
+ # @api semipublic
205
585
  def initialize(repository, model, options = {})
206
586
  assert_kind_of 'repository', repository, Repository
207
587
  assert_kind_of 'model', model, Model
208
- assert_kind_of 'options', options, Hash
209
588
 
210
- options.each_pair { |k,v| options[k] = v.call if v.is_a? Proc } if options.is_a? Hash
589
+ @repository = repository
590
+ @model = model
591
+ @options = options.dup.freeze
592
+
593
+ repository_name = repository.name
211
594
 
212
- assert_valid_options(options)
595
+ @properties = @model.properties(repository_name)
596
+ @relationships = @model.relationships(repository_name)
213
597
 
214
- @repository = repository
215
- @properties = model.properties(@repository.name)
216
-
217
- @model = model # must be Class that includes DM::Resource
218
- @reload = options.fetch :reload, false # must be true or false
219
- @unique = options.fetch :unique, false # must be true or false
220
- @offset = options.fetch :offset, 0 # must be an Integer greater than or equal to 0
221
- @limit = options.fetch :limit, nil # must be an Integer greater than or equal to 1
222
- @order = options.fetch :order, model.default_order(@repository.name) # must be an Array of Symbol, DM::Query::Direction or DM::Property
223
- @add_reversed = options.fetch :add_reversed, false # must be true or false
224
- @fields = options.fetch :fields, @properties.defaults # must be an Array of Symbol, String or DM::Property
225
- @links = options.fetch :links, [] # must be an Array of Tuples - Tuple [DM::Query,DM::Assoc::Relationship]
226
- @includes = options.fetch :includes, [] # must be an Array of DM::Query::Path
227
- @conditions = [] # must be an Array of triplets (or pairs when passing in raw String queries)
228
-
229
- # normalize order and fields
230
- @order = normalize_order(@order)
231
- @fields = normalize_fields(@fields)
232
-
233
- # XXX: should I validate that each property in @order corresponds
234
- # to something in @fields? Many DB engines require they match,
235
- # and I can think of no valid queries where a field would be so
236
- # important that you sort on it, but not important enough to
237
- # return.
238
-
239
- # normalize links and includes.
240
- # NOTE: this must be done after order and fields
241
- @links = normalize_links(@links)
242
- @includes = normalize_includes(@includes)
598
+ assert_valid_options(@options)
599
+
600
+ @fields = @options.fetch :fields, @properties.defaults
601
+ @links = @options.fetch :links, []
602
+ @conditions = Conditions::Operation.new(:null)
603
+ @offset = @options.fetch :offset, 0
604
+ @limit = @options.fetch :limit, nil
605
+ @order = @options.fetch :order, @model.default_order(repository_name)
606
+ @unique = @options.fetch :unique, false
607
+ @add_reversed = @options.fetch :add_reversed, false
608
+ @reload = @options.fetch :reload, false
609
+ @raw = false
610
+
611
+ @links = @links.dup
243
612
 
244
613
  # treat all non-options as conditions
245
- (options.keys - OPTIONS).each do |k|
246
- append_condition(k, options[k])
247
- end
614
+ @options.except(*OPTIONS).each { |kv| append_condition(*kv) }
248
615
 
249
- # parse raw options[:conditions] differently
250
- if conditions = options[:conditions]
251
- if conditions.kind_of?(Hash)
252
- conditions.each do |k,v|
253
- append_condition(k, v)
254
- end
255
- elsif conditions.kind_of?(Array)
256
- raw_query, *bind_values = conditions
257
- @conditions << if bind_values.empty?
258
- [ :raw, raw_query ]
259
- else
260
- [ :raw, raw_query, bind_values ]
261
- end
262
- end
616
+ # parse @options[:conditions] differently
617
+ case conditions = @options[:conditions]
618
+ when Conditions::AbstractOperation, Conditions::AbstractComparison
619
+ add_condition(conditions)
620
+
621
+ when Hash
622
+ conditions.each { |kv| append_condition(*kv) }
623
+
624
+ when Array
625
+ statement, *bind_values = *conditions
626
+ add_condition([ statement, bind_values ])
627
+ @raw = true
263
628
  end
629
+
630
+ normalize_order
631
+ normalize_fields
632
+ normalize_links
264
633
  end
265
634
 
635
+ # Copying contructor, called for Query#dup
636
+ #
637
+ # @api semipublic
266
638
  def initialize_copy(original)
267
- # deep-copy the condition tuples when copying the object
268
- @conditions = original.conditions.map { |tuple| tuple.dup }
639
+ initialize(original.repository, original.model, original.options)
269
640
  end
270
641
 
271
- # validate the options
642
+ # Validate the options
643
+ #
644
+ # @param [#each] options
645
+ # the options to validate
646
+ #
647
+ # @raise [ArgumentError]
648
+ # if any pairs in +options+ are invalid options
649
+ #
650
+ # @api private
272
651
  def assert_valid_options(options)
273
- # [DB] This might look more ugly now, but it's 2x as fast as the old code
274
- # [DB] This is one of the heavy spots for Query.new I found during profiling.
275
- options.each_pair do |attribute, value|
276
-
277
- # validate the reload option and unique option
278
- if [:reload, :unique].include? attribute
279
- if value != true && value != false
280
- raise ArgumentError, "+options[:#{attribute}]+ must be true or false, but was #{value.inspect}", caller(2)
281
- end
652
+ assert_kind_of 'options', options, Hash
653
+
654
+ options.each do |attribute, value|
655
+ case attribute
656
+ when :fields then assert_valid_fields(value, options[:unique])
657
+ when :links then assert_valid_links(value)
658
+ when :conditions then assert_valid_conditions(value)
659
+ when :offset then assert_valid_offset(value, options[:limit])
660
+ when :limit then assert_valid_limit(value)
661
+ when :order then assert_valid_order(value, options[:fields])
662
+ when :unique, :add_reversed, :reload then assert_valid_boolean("options[:#{attribute}]", value)
663
+ else
664
+ assert_valid_conditions(attribute => value)
665
+ end
666
+ end
667
+ end
282
668
 
283
- # validate the offset and limit options
284
- elsif [:offset, :limit].include? attribute
285
- assert_kind_of "options[:#{attribute}]", value, Integer
286
- if attribute == :offset && value < 0
287
- raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{value.inspect}", caller(2)
288
- elsif attribute == :limit && value < 1
289
- raise ArgumentError, "+options[:limit]+ must be greater than or equal to 1, but was #{options[:limit].inspect}", caller(2)
290
- end
669
+ # Verifies that value of :fields option
670
+ # refers to existing properties
671
+ #
672
+ # @api private
673
+ def assert_valid_fields(fields, unique)
674
+ assert_kind_of 'options[:fields]', fields, Array
291
675
 
292
- # validate the :order, :fields, :links and :includes options
293
- elsif [ :order, :fields, :links, :includes ].include? attribute
294
- assert_kind_of "options[:#{attribute}]", value, Array
295
-
296
- if value.empty?
297
- if attribute == :fields
298
- if options[:unique] == false
299
- raise ArgumentError, '+options[:fields]+ cannot be empty if +options[:unique] is false', caller(2)
300
- end
301
- elsif attribute == :order
302
- if options[:fields] && options[:fields].any? { |p| !p.kind_of?(Operator) }
303
- raise ArgumentError, '+options[:order]+ cannot be empty if +options[:fields] contains a non-operator', caller(2)
304
- end
305
- else
306
- raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
676
+ if fields.empty? && unique == false
677
+ raise ArgumentError, '+options[:fields]+ should not be empty if +options[:unique]+ is false'
678
+ end
679
+
680
+ fields.each do |field|
681
+ case field
682
+ when Symbol, String
683
+ unless @properties.named?(field)
684
+ raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
307
685
  end
308
- end
309
686
 
310
- # validates the :conditions option
311
- elsif :conditions == attribute
312
- assert_kind_of 'options[:conditions]', value, Hash, Array
687
+ when Property
688
+ unless @properties.include?(field)
689
+ raise ArgumentError, "+options[:field]+ entry #{field.name.inspect} does not map to a property in #{model}"
690
+ end
313
691
 
314
- if value.empty?
315
- raise ArgumentError, '+options[:conditions]+ cannot be empty', caller(2)
316
- end
692
+ else
693
+ raise ArgumentError, "+options[:fields]+ entry #{field.inspect} of an unsupported object #{field.class}"
317
694
  end
318
695
  end
319
696
  end
320
697
 
321
- # validate other DM::Query or Hash object
322
- def assert_valid_other(other)
323
- return unless other.kind_of?(self.class)
698
+ # Verifies that value of :links option
699
+ # refers to existing associations
700
+ #
701
+ # @api private
702
+ def assert_valid_links(links)
703
+ assert_kind_of 'options[:links]', links, Array
324
704
 
325
- unless other.repository == repository
326
- raise ArgumentError, "+other+ #{self.class} must be for the #{repository.name} repository, not #{other.repository.name}", caller(2)
705
+ if links.empty?
706
+ raise ArgumentError, '+options[:links]+ should not be empty'
327
707
  end
328
708
 
329
- unless other.model == model
330
- raise ArgumentError, "+other+ #{self.class} must be for the #{model.name} model, not #{other.model.name}", caller(2)
709
+ links.each do |link|
710
+ case link
711
+ when Symbol, String
712
+ unless @relationships.key?(link.to_sym)
713
+ raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
714
+ end
715
+
716
+ when Associations::Relationship
717
+ # TODO: figure out how to validate links from other models
718
+ #unless @relationships.value?(link)
719
+ # raise ArgumentError, "+options[:links]+ entry #{link.name.inspect} does not map to a relationship in #{model}"
720
+ #end
721
+
722
+ else
723
+ raise ArgumentError, "+options[:links]+ entry #{link.inspect} of an unsupported object #{link.class}"
724
+ end
331
725
  end
332
726
  end
333
727
 
334
- # normalize order elements to DM::Query::Direction
335
- def normalize_order(order)
336
- order.map do |order_by|
337
- case order_by
338
- when Direction
339
- # NOTE: The property is available via order_by.property
340
- # TODO: if the Property's model doesn't match
341
- # self.model, append the property's model to @links
342
- # eg:
343
- #if property.model != self.model
344
- # @links << discover_path_for_property(property)
345
- #end
728
+ # Verifies that value of :conditions option
729
+ # refers to existing properties
730
+ #
731
+ # @api private
732
+ def assert_valid_conditions(conditions)
733
+ assert_kind_of 'options[:conditions]', conditions, Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array
734
+
735
+ case conditions
736
+ when Hash
737
+ conditions.each do |subject, bind_value|
738
+ case subject
739
+ when Symbol, String
740
+ unless subject.to_s.include?('.') || @properties.named?(subject) || @relationships.key?(subject)
741
+ raise ArgumentError, "condition #{subject.inspect} does not map to a property or relationship in #{model}"
742
+ end
346
743
 
347
- order_by
348
- when Property
349
- # TODO: if the Property's model doesn't match
350
- # self.model, append the property's model to @links
351
- # eg:
352
- #if property.model != self.model
353
- # @links << discover_path_for_property(property)
354
- #end
744
+ when Operator
745
+ unless (Conditions::Comparison.slugs | [ :not ]).include?(subject.operator)
746
+ raise ArgumentError, "condition #{subject.inspect} used an invalid operator #{subject.operator}"
747
+ end
355
748
 
356
- Direction.new(order_by)
357
- when Operator
358
- property = @properties[order_by.target]
359
- Direction.new(property, order_by.operator)
360
- when Symbol, String
361
- property = @properties[order_by]
749
+ assert_valid_conditions(subject.target => bind_value)
750
+
751
+ if subject.operator == :not && bind_value.kind_of?(Array) && bind_value.empty?
752
+ raise ArgumentError, "Cannot use 'not' operator with a bind value that is an empty Array for #{subject.inspect}"
753
+ end
754
+
755
+ when Path
756
+ assert_valid_links(subject.relationships)
362
757
 
363
- if property.nil?
364
- raise ArgumentError, "+options[:order]+ entry #{order_by} does not map to a DataMapper::Property", caller(2)
758
+ when Associations::Relationship, Property
759
+ # TODO: validate that it belongs to the current model, or to any
760
+ # model in the links
761
+ #unless @properties.include?(subject)
762
+ # raise ArgumentError, "condition #{subject.name.inspect} does not map to a property in #{model}"
763
+ #end
764
+
765
+ else
766
+ raise ArgumentError, "condition #{subject.inspect} of an unsupported object #{subject.class}"
365
767
  end
768
+ end
366
769
 
367
- Direction.new(property)
368
- else
369
- raise ArgumentError, "+options[:order]+ entry #{order_by.inspect} not supported", caller(2)
370
- end
770
+ when Array
771
+ if conditions.empty?
772
+ raise ArgumentError, '+options[:conditions]+ should not be empty'
773
+ end
774
+
775
+ unless conditions.first.kind_of?(String) && !conditions.first.blank?
776
+ raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
777
+ end
371
778
  end
372
779
  end
373
780
 
374
- # normalize fields to DM::Property
375
- def normalize_fields(fields)
376
- # TODO: return a PropertySet
377
- # TODO: raise an exception if the property is not available in the repository
378
- fields.map do |field|
379
- case field
380
- when Property, Operator
381
- # TODO: if the Property's model doesn't match
382
- # self.model, append the property's model to @links
383
- # eg:
384
- #if property.model != self.model
385
- # @links << discover_path_for_property(property)
386
- #end
387
- field
388
- when Symbol, String
389
- property = @properties[field]
781
+ # Verifies that query offset is non-negative and only used together with limit
782
+ # @api private
783
+ def assert_valid_offset(offset, limit)
784
+ assert_kind_of 'options[:offset]', offset, Integer
390
785
 
391
- if property.nil?
392
- raise ArgumentError, "+options[:fields]+ entry #{field} does not map to a DataMapper::Property", caller(2)
393
- end
786
+ unless offset >= 0
787
+ raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{offset.inspect}"
788
+ end
394
789
 
395
- property
396
- else
397
- raise ArgumentError, "+options[:fields]+ entry #{field.inspect} not supported", caller(2)
398
- end
790
+ if offset > 0 && limit.nil?
791
+ raise ArgumentError, '+options[:offset]+ cannot be greater than 0 if limit is not specified'
399
792
  end
400
793
  end
401
794
 
402
- # normalize links to DM::Query::Path
403
- def normalize_links(links)
404
- # XXX: this should normalize to DM::Query::Path, not DM::Association::Relationship
405
- # because a link may be more than one-hop-away from the source. A DM::Query::Path
406
- # should include an Array of Relationship objects that trace the "path" between
407
- # the source and the target.
408
- links.map do |link|
409
- case link
410
- when Associations::Relationship
411
- link
795
+ # Verifies the limit is equal to or greater than 0
796
+ #
797
+ # @raise [ArgumentError]
798
+ # raised if the limit is not an Integer or less than 0
799
+ #
800
+ # @api private
801
+ def assert_valid_limit(limit)
802
+ assert_kind_of 'options[:limit]', limit, Integer
803
+
804
+ unless limit >= 0
805
+ raise ArgumentError, "+options[:limit]+ must be greater than or equal to 0, but was #{limit.inspect}"
806
+ end
807
+ end
808
+
809
+ # Verifies that :order option uses proper operator and refers
810
+ # to existing property
811
+ #
812
+ # @api private
813
+ def assert_valid_order(order, fields)
814
+ return if order.nil?
815
+
816
+ assert_kind_of 'options[:order]', order, Array
817
+
818
+ if order.empty? && fields && fields.any? { |property| !property.kind_of?(Operator) }
819
+ raise ArgumentError, '+options[:order]+ should not be empty if +options[:fields] contains a non-operator'
820
+ end
821
+
822
+ order.each do |order_entry|
823
+ case order_entry
412
824
  when Symbol, String
413
- link = link.to_sym if link.kind_of?(String)
825
+ unless @properties.named?(order_entry)
826
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
827
+ end
828
+
829
+ when Property
830
+ unless @properties.include?(order_entry)
831
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.name.inspect} does not map to a property in #{model}"
832
+ end
414
833
 
415
- unless model.relationships(@repository.name).has_key?(link)
416
- raise ArgumentError, "+options[:links]+ entry #{link} does not map to a DataMapper::Associations::Relationship", caller(2)
834
+ when Operator, Direction
835
+ unless order_entry.operator == :asc || order_entry.operator == :desc
836
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} used an invalid operator #{order_entry.operator}"
417
837
  end
418
838
 
419
- model.relationships(@repository.name)[link]
839
+ assert_valid_order([ order_entry.target ], fields)
840
+
420
841
  else
421
- raise ArgumentError, "+options[:links]+ entry #{link.inspect} not supported", caller(2)
842
+ raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} of an unsupported object #{order_entry.class}"
422
843
  end
423
844
  end
424
845
  end
425
846
 
426
- # normalize includes to DM::Query::Path
427
- def normalize_includes(includes)
428
- # TODO: normalize Array of Symbol, String, DM::Property 1-jump-away or DM::Query::Path
429
- # NOTE: :includes can only be and array of DM::Query::Path objects now. This method
430
- # can go away after review of what has been done.
431
- includes
847
+ # Used to verify value of boolean properties in conditions
848
+ # @api private
849
+ def assert_valid_boolean(name, value)
850
+ if value != true && value != false
851
+ raise ArgumentError, "+#{name}+ should be true or false, but was #{value.inspect}"
852
+ end
432
853
  end
433
854
 
434
- # validate that all the links or includes are present for the given DM::Query::Path
855
+ # Verifies that associations given in conditions belong
856
+ # to the same repository as query's model
435
857
  #
436
- def validate_query_path_links(path)
437
- path.relationships.map do |relationship|
438
- @links << relationship unless (@links.include?(relationship) || @includes.include?(relationship))
858
+ # @api private
859
+ def assert_valid_other(other)
860
+ unless other.repository == repository
861
+ raise ArgumentError, "+other+ #{other.class} must be for the #{repository.name} repository, not #{other.repository.name}"
862
+ end
863
+
864
+ unless other.model >= model
865
+ raise ArgumentError, "+other+ #{other.class} must be for the #{model.name} model, not #{other.model.name}"
439
866
  end
440
867
  end
441
868
 
442
- def append_condition(clause, bind_value)
443
- operator = :eql
444
- bind_value = bind_value.call if bind_value.is_a?(Proc)
869
+ # Normalize order elements to Query::Direction instances
870
+ #
871
+ # TODO: needs example
872
+ #
873
+ # @api private
874
+ def normalize_order
875
+ return if @order.nil?
445
876
 
446
- property = case clause
447
- when Property
448
- clause
449
- when Query::Path
450
- validate_query_path_links(clause)
451
- clause
452
- when Operator
453
- operator = clause.operator
454
- return if operator == :not && bind_value == []
455
- if clause.target.is_a?(Symbol)
456
- @properties[clause.target]
457
- elsif clause.target.is_a?(Query::Path)
458
- validate_query_path_links(clause.target)
459
- clause.target
460
- end
461
- when Symbol
462
- @properties[clause]
463
- when String
464
- if clause =~ /\w\.\w/
465
- query_path = @model
466
- clause.split(".").each { |piece| query_path = query_path.send(piece) }
467
- append_condition(query_path, bind_value)
468
- return
469
- else
470
- @properties[clause]
471
- end
472
- else
473
- raise ArgumentError, "Condition type #{clause.inspect} not supported", caller(2)
474
- end
877
+ # TODO: should Query::Path objects be permitted? If so, then it
878
+ # should probably be normalized to a Direction object
879
+ @order = @order.map do |order|
880
+ case order
881
+ when Operator
882
+ target = order.target
883
+ property = target.kind_of?(Property) ? target : @properties[target]
475
884
 
476
- if property.nil?
477
- raise ArgumentError, "Clause #{clause.inspect} does not map to a DataMapper::Property", caller(2)
478
- end
885
+ Direction.new(property, order.operator)
479
886
 
480
- bind_value = dump_custom_value(property, bind_value)
887
+ when Symbol, String
888
+ Direction.new(@properties[order])
481
889
 
482
- @conditions << [ operator, property, bind_value ]
890
+ when Property
891
+ Direction.new(order)
892
+
893
+ when Direction
894
+ order.dup
895
+ end
896
+ end
483
897
  end
484
898
 
485
- def dump_custom_value(property_or_path, bind_value)
486
- case property_or_path
487
- when DataMapper::Query::Path
488
- dump_custom_value(property_or_path.property, bind_value)
489
- when Property
490
- if property_or_path.custom?
491
- property_or_path.type.dump(bind_value, property_or_path)
492
- else
493
- bind_value
899
+ # Normalize fields to Property instances
900
+ #
901
+ # TODO: needs example
902
+ #
903
+ # @api private
904
+ def normalize_fields
905
+ @fields = @fields.map do |field|
906
+ case field
907
+ when Symbol, String
908
+ @properties[field]
909
+
910
+ when Property, Operator
911
+ field
494
912
  end
495
- else
496
- bind_value
497
- end
498
- end
499
-
500
- # TODO: check for other mutually exclusive operator + property
501
- # combinations. For example if self's conditions were
502
- # [ :gt, :amount, 5 ] and the other's condition is [ :lt, :amount, 2 ]
503
- # there is a conflict. When in conflict the other's conditions
504
- # overwrites self's conditions.
505
-
506
- # TODO: Another condition is when the other condition operator is
507
- # eql, this should over-write all the like,range and list operators
508
- # for the same property, since we are now looking for an exact match.
509
- # Vice versa, passing in eql should overwrite all of those operators.
510
-
511
- def update_conditions(other)
512
- @conditions = @conditions.dup
513
-
514
- # build an index of conditions by the property and operator to
515
- # avoid nested looping
516
- conditions_index = {}
517
- @conditions.each do |condition|
518
- operator, property = *condition
519
- next if :raw == operator
520
- conditions_index[property] ||= {}
521
- conditions_index[property][operator] = condition
522
- end
523
-
524
- # loop over each of the other's conditions, and overwrite the
525
- # conditions when in conflict
526
- other.conditions.each do |other_condition|
527
- other_operator, other_property, other_bind_value = *other_condition
528
-
529
- unless :raw == other_operator
530
- conditions_index[other_property] ||= {}
531
- if condition = conditions_index[other_property][other_operator]
532
- operator, property, bind_value = *condition
533
-
534
- next if bind_value == other_bind_value
535
-
536
- # overwrite the bind value in the existing condition
537
- condition[2] = case operator
538
- when :eql, :like then other_bind_value
539
- when :gt, :gte then [ bind_value, other_bind_value ].min
540
- when :lt, :lte then [ bind_value, other_bind_value ].max
541
- when :not, :in
542
- if bind_value.kind_of?(Array)
543
- bind_value |= other_bind_value
544
- elsif other_bind_value.kind_of?(Array)
545
- other_bind_value |= bind_value
546
- else
547
- other_bind_value
548
- end
549
- end
913
+ end
914
+ end
550
915
 
551
- next # process the next other condition
552
- end
916
+ # Normalize links to Query::Path
917
+ #
918
+ # Normalization means links given as symbols are replaced with
919
+ # relationships they refer to, intermediate links are "followed"
920
+ # and duplicates are removed
921
+ #
922
+ # @api private
923
+ def normalize_links
924
+ links = @links.dup
925
+
926
+ @links.clear
927
+
928
+ while link = links.shift
929
+ relationship = case link
930
+ when Symbol, String then @relationships[link]
931
+ when Associations::Relationship then link
553
932
  end
554
933
 
555
- # otherwise append the other condition
556
- @conditions << other_condition.dup
557
- end
934
+ next if @links.include?(relationship)
558
935
 
559
- @conditions
560
- end
936
+ if relationship.respond_to?(:links)
937
+ links.concat(relationship.links)
938
+ else
939
+ repository_name = relationship.relative_target_repository_name
940
+ model = relationship.target_model
561
941
 
562
- class Direction
563
- include Assertions
942
+ # TODO: see if this can handle extracting the :order option and sort the
943
+ # resulting collection using the order specified by through relationships
564
944
 
565
- attr_reader :property, :direction
945
+ model.current_scope.merge(relationship.query).each do |subject, value|
946
+ # TODO: figure out how to merge Query options from links
947
+ if OPTIONS.include?(subject)
948
+ next # skip for now
949
+ end
566
950
 
567
- def ==(other)
568
- return true if super
569
- hash == other.hash
570
- end
951
+ # set @repository when appending conditions
952
+ original, @repository = @repository, DataMapper.repository(repository_name)
571
953
 
572
- alias eql? ==
954
+ begin
955
+ append_condition(subject, value, model)
956
+ ensure
957
+ @repository = original
958
+ end
959
+ end
573
960
 
574
- def hash
575
- @property.hash + @direction.hash
961
+ @links << relationship
962
+ end
576
963
  end
964
+ end
577
965
 
578
- def reverse
579
- self.class.new(@property, @direction == :asc ? :desc : :asc)
966
+ # Append conditions to this Query
967
+ #
968
+ # TODO: needs example
969
+ #
970
+ # @param [Property, Symbol, String, Operator, Associations::Relationship, Path] subject
971
+ # the subject to match
972
+ # @param [Object] bind_value
973
+ # the value to match on
974
+ # @param [Symbol] operator
975
+ # the operator to match with
976
+ #
977
+ # @return [Query::Conditions::AbstractOperation]
978
+ # the Query conditions
979
+ #
980
+ # @api private
981
+ def append_condition(subject, bind_value, model = self.model, operator = :eql)
982
+ case subject
983
+ when Property, Associations::Relationship then append_property_condition(subject, bind_value, operator)
984
+ when Symbol then append_symbol_condition(subject, bind_value, model, operator)
985
+ when String then append_string_condition(subject, bind_value, model, operator)
986
+ when Operator then append_operator_conditions(subject, bind_value, model)
987
+ when Path then append_path(subject, bind_value, model, operator)
988
+ else
989
+ raise ArgumentError, "#{subject} is an invalid instance: #{subject.class}"
580
990
  end
991
+ end
581
992
 
582
- def inspect
583
- "#<#{self.class.name} #{@property.inspect} #{@direction}>"
993
+ # TODO: document
994
+ # @api private
995
+ def append_property_condition(property, bind_value, operator)
996
+ bind_value = normalize_bind_value(property, bind_value)
997
+ negated = operator == :not
998
+
999
+ if operator == :eql || negated
1000
+ operator = case bind_value
1001
+ when Array, Range, Set, Collection then :in
1002
+ when Regexp then :regexp
1003
+ else :eql
1004
+ end
584
1005
  end
585
1006
 
586
- private
587
-
588
- def initialize(property, direction = :asc)
589
- assert_kind_of 'property', property, Property
590
- assert_kind_of 'direction', direction, Symbol
1007
+ condition = Conditions::Comparison.new(operator, property, bind_value)
591
1008
 
592
- @property = property
593
- @direction = direction
1009
+ if negated
1010
+ condition = Conditions::Operation.new(:not, condition)
594
1011
  end
595
- end # class Direction
596
-
597
- class Operator
598
- include Assertions
599
1012
 
600
- attr_reader :target, :operator
1013
+ add_condition(condition)
1014
+ end
601
1015
 
602
- def to_sym
603
- @property_name
604
- end
1016
+ # TODO: document
1017
+ # @api private
1018
+ def append_symbol_condition(symbol, bind_value, model, operator)
1019
+ append_condition(symbol.to_s, bind_value, model, operator)
1020
+ end
605
1021
 
606
- def ==(other)
607
- return true if super
608
- return false unless other.kind_of?(self.class)
609
- @operator == other.operator && @target == other.target
610
- end
1022
+ # TODO: document
1023
+ # @api private
1024
+ def append_string_condition(string, bind_value, model, operator)
1025
+ if string.include?('.')
1026
+ query_path = model
611
1027
 
612
- private
1028
+ target_components = string.split('.')
1029
+ operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.map{ |s| s.to_s }.include? target_components.last
1030
+ target_components.each { |method| query_path = query_path.send(method) }
613
1031
 
614
- def initialize(target, operator)
615
- assert_kind_of 'operator', operator, Symbol
1032
+ append_condition(query_path, bind_value, model, operator)
1033
+ else
1034
+ repository_name = repository.name
1035
+ subject = model.properties(repository_name)[string] ||
1036
+ model.relationships(repository_name)[string]
616
1037
 
617
- @target = target
618
- @operator = operator
1038
+ append_condition(subject, bind_value, model, operator)
619
1039
  end
620
- end # class Operator
1040
+ end
621
1041
 
622
- class Path
623
- include Assertions
1042
+ # TODO: document
1043
+ # @api private
1044
+ def append_operator_conditions(operator, bind_value, model)
1045
+ append_condition(operator.target, bind_value, model, operator.operator)
1046
+ end
624
1047
 
625
- instance_methods.each { |m| undef_method m if %w[ id type ].include?(m.to_s) }
1048
+ # TODO: document
1049
+ # @api private
1050
+ def append_path(path, bind_value, model, operator)
1051
+ @links.unshift(*path.relationships.reverse.map { |relationship| relationship.inverse })
1052
+ append_condition(path.property, bind_value, path.model, operator)
1053
+ end
626
1054
 
627
- attr_reader :relationships, :model, :property, :operator
1055
+ # Add a condition to the Query
1056
+ #
1057
+ # @param [AbstractOperation, AbstractComparison]
1058
+ # the condition to add to the Query
1059
+ #
1060
+ # @return [undefined]
1061
+ #
1062
+ # @api private
1063
+ def add_condition(condition)
1064
+ @conditions = Conditions::Operation.new(:and) if @conditions.nil?
1065
+ @conditions << condition
1066
+ end
628
1067
 
629
- [ :gt, :gte, :lt, :lte, :not, :eql, :like, :in ].each do |sym|
630
- class_eval <<-EOS, __FILE__, __LINE__
631
- def #{sym}
632
- Operator.new(self, :#{sym})
633
- end
634
- EOS
1068
+ # TODO: make this typecast all bind values that do not match the
1069
+ # property primitive
1070
+
1071
+ # TODO: document
1072
+ # @api private
1073
+ def normalize_bind_value(property_or_path, bind_value)
1074
+ # TODO: defer this inside the comparison
1075
+ if bind_value.respond_to?(:call)
1076
+ bind_value = bind_value.call
635
1077
  end
636
1078
 
637
- # duck type the DM::Query::Path to act like a DM::Property
638
- def field(*args)
639
- @property ? @property.field(*args) : nil
1079
+ # TODO: bypass this for Collection, once subqueries can be handled by adapters
1080
+ if bind_value.respond_to?(:to_ary)
1081
+ bind_value = bind_value.to_ary
1082
+ bind_value.uniq!
640
1083
  end
641
1084
 
642
- # more duck typing
643
- def to_sym
644
- @property ? @property.name.to_sym : @model.storage_name(@repository).to_sym
1085
+ # FIXME: causes m:m specs to fail with in-memory adapter
1086
+ # if bind_value.instance_of?(Array) && bind_value.size == 1
1087
+ # bind_value = bind_value.first
1088
+ # end
1089
+
1090
+ bind_value
1091
+ end
1092
+
1093
+ # Extract arguments for #slice and #slice! then return offset and limit
1094
+ #
1095
+ # @param [Integer, Array(Integer), Range] *args the offset,
1096
+ # offset and limit, or range indicating first and last position
1097
+ #
1098
+ # @return [Integer] the offset
1099
+ # @return [Integer, NilClass] the limit, if any
1100
+ #
1101
+ # @api private
1102
+ def extract_slice_arguments(*args)
1103
+ first_arg, second_arg = args
1104
+
1105
+ if args.size == 2 && first_arg.kind_of?(Integer) && second_arg.kind_of?(Integer)
1106
+ return first_arg, second_arg
1107
+ elsif args.size == 1
1108
+ if first_arg.kind_of?(Integer)
1109
+ return first_arg, 1
1110
+ elsif first_arg.kind_of?(Range)
1111
+ offset = first_arg.first
1112
+ limit = first_arg.last - offset
1113
+ limit += 1 unless first_arg.exclude_end?
1114
+ return offset, limit
1115
+ end
645
1116
  end
646
1117
 
647
- private
1118
+ raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
1119
+ end
1120
+
1121
+ # TODO: document
1122
+ # @api private
1123
+ def get_relative_position(offset, limit)
1124
+ new_offset = self.offset + offset
1125
+
1126
+ if limit <= 0 || (self.limit && new_offset + limit > self.offset + self.limit)
1127
+ raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
1128
+ end
648
1129
 
649
- def initialize(repository, relationships, model, property_name = nil)
650
- assert_kind_of 'repository', repository, Repository
651
- assert_kind_of 'relationships', relationships, Array
652
- assert_kind_of 'model', model, Model
653
- assert_kind_of 'property_name', property_name, Symbol unless property_name.nil?
1130
+ return new_offset, limit
1131
+ end
654
1132
 
655
- @repository = repository
656
- @relationships = relationships
657
- @model = model
658
- @property = @model.properties(@repository.name)[property_name] if property_name
1133
+ # TODO: DRY this up with conditions
1134
+ # @api private
1135
+ def record_value(record, property)
1136
+ case record
1137
+ when Hash
1138
+ record.fetch(property, record[property.field])
1139
+ when Resource
1140
+ property.get!(record)
659
1141
  end
1142
+ end
660
1143
 
661
- def method_missing(method, *args)
662
- if relationship = @model.relationships(@repository.name)[method]
663
- klass = klass = model == relationship.child_model ? relationship.parent_model : relationship.child_model
664
- return Query::Path.new(@repository, @relationships + [ relationship ], klass)
665
- end
1144
+ # TODO: document
1145
+ # @api private
1146
+ def each_comparison
1147
+ operands = conditions.operands.dup
666
1148
 
667
- if @model.properties(@repository.name)[method]
668
- @property = @model.properties(@repository.name)[method] unless @property
669
- return self
1149
+ while operand = operands.shift
1150
+ if operand.respond_to?(:operands)
1151
+ operands.concat(operand.operands)
1152
+ else
1153
+ yield operand
670
1154
  end
671
-
672
- raise NoMethodError, "undefined property or association `#{method}' on #{@model}"
673
1155
  end
674
- end # class Path
1156
+ end
675
1157
  end # class Query
676
1158
  end # module DataMapper