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